Skip to content

Commit 0fceb14

Browse files
authored
[Type] add non_empty_dict (#201)
1 parent 0a3919f commit 0fceb14

File tree

5 files changed

+249
-0
lines changed

5 files changed

+249
-0
lines changed

docs/component/type.md

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
- [mixed](./../../src/Psl/Type/mixed.php#L10)
4040
- [mutable_map](./../../src/Psl/Type/mutable_map.php#L21)
4141
- [mutable_vector](./../../src/Psl/Type/mutable_vector.php#L19)
42+
- [non_empty_dict](./../../src/Psl/Type/non_empty_dict.php#L20)
4243
- [non_empty_string](./../../src/Psl/Type/non_empty_string.php#L10)
4344
- [non_empty_vec](./../../src/Psl/Type/non_empty_vec.php#L18)
4445
- [null](./../../src/Psl/Type/null.php#L10)

src/Psl/Internal/Loader.php

+2
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ final class Loader
400400
'Psl\Type\object',
401401
'Psl\Type\resource',
402402
'Psl\Type\string',
403+
'Psl\Type\non_empty_dict',
403404
'Psl\Type\non_empty_string',
404405
'Psl\Type\non_empty_vec',
405406
'Psl\Type\scalar',
@@ -616,6 +617,7 @@ final class Loader
616617
'Psl\Type\Internal\ResourceType',
617618
'Psl\Type\Internal\StringType',
618619
'Psl\Type\Internal\ShapeType',
620+
'Psl\Type\Internal\NonEmptyDictType',
619621
'Psl\Type\Internal\NonEmptyStringType',
620622
'Psl\Type\Internal\NonEmptyVecType',
621623
'Psl\Type\Internal\UnionType',
+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\Type\Internal;
6+
7+
use Psl;
8+
use Psl\Str;
9+
use Psl\Type;
10+
use Psl\Type\Exception\AssertException;
11+
use Psl\Type\Exception\CoercionException;
12+
13+
use function is_array;
14+
use function is_iterable;
15+
16+
/**
17+
* @template Tk of array-key
18+
* @template Tv
19+
*
20+
* @extends Type\Type<non-empty-array<Tk, Tv>>
21+
*
22+
* @internal
23+
*/
24+
final class NonEmptyDictType extends Type\Type
25+
{
26+
/**
27+
* @param Type\TypeInterface<Tk> $key_type
28+
* @param Type\TypeInterface<Tv> $value_type
29+
*
30+
* @throws Psl\Exception\InvariantViolationException If $key_value, or $value_type is optional.
31+
*/
32+
public function __construct(
33+
private Type\TypeInterface $key_type,
34+
private Type\TypeInterface $value_type
35+
) {
36+
Psl\invariant(
37+
!$key_type->isOptional() && !$value_type->isOptional(),
38+
'Optional type must be the outermost.'
39+
);
40+
}
41+
42+
/**
43+
* @throws CoercionException
44+
*
45+
* @return non-empty-array<Tk, Tv>
46+
*/
47+
public function coerce(mixed $value): array
48+
{
49+
if (is_iterable($value)) {
50+
$key_trace = $this->getTrace()
51+
->withFrame(Str\format('non-empty-dict<%s, _>', $this->key_type->toString()));
52+
$value_trace = $this->getTrace()
53+
->withFrame(Str\format('non-empty-dict<_, %s>', $this->value_type->toString()));
54+
55+
$key_type = $this->key_type->withTrace($key_trace);
56+
$value_type = $this->value_type->withTrace($value_trace);
57+
58+
$result = [];
59+
60+
/**
61+
* @var Tk $k
62+
* @var Tv $v
63+
*/
64+
foreach ($value as $k => $v) {
65+
$result[$key_type->coerce($k)] = $value_type->coerce($v);
66+
}
67+
68+
if ($result === []) {
69+
throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
70+
}
71+
72+
return $result;
73+
}
74+
75+
throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
76+
}
77+
78+
/**
79+
* @throws AssertException
80+
*
81+
* @return non-empty-array<Tk, Tv>
82+
*
83+
* @psalm-assert non-empty-array<Tk, Tv> $value
84+
*/
85+
public function assert(mixed $value): array
86+
{
87+
if (is_array($value)) {
88+
$key_trace = $this->getTrace()
89+
->withFrame(Str\format('non-empty-dict<%s, _>', $this->key_type->toString()));
90+
$value_trace = $this->getTrace()
91+
->withFrame(Str\format('non-empty-dict<_, %s>', $this->value_type->toString()));
92+
93+
$key_type = $this->key_type->withTrace($key_trace);
94+
$value_type = $this->value_type->withTrace($value_trace);
95+
96+
$result = [];
97+
98+
/**
99+
* @var Tk $k
100+
* @var Tv $v
101+
*/
102+
foreach ($value as $k => $v) {
103+
$result[$key_type->assert($k)] = $value_type->assert($v);
104+
}
105+
106+
if ($result === []) {
107+
throw AssertException::withValue($value, $this->toString(), $this->getTrace());
108+
}
109+
110+
return $result;
111+
}
112+
113+
throw AssertException::withValue($value, $this->toString(), $this->getTrace());
114+
}
115+
116+
public function toString(): string
117+
{
118+
return Str\format('non-empty-dict<%s, %s>', $this->key_type->toString(), $this->value_type->toString());
119+
}
120+
}

src/Psl/Type/non_empty_dict.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\Type;
6+
7+
use Psl;
8+
9+
/**
10+
* @template Tk of array-key
11+
* @template Tv
12+
*
13+
* @param TypeInterface<Tk> $key_type
14+
* @param TypeInterface<Tv> $value_type
15+
*
16+
* @throws Psl\Exception\InvariantViolationException If $key_value, or $value_type is optional.
17+
*
18+
* @return TypeInterface<non-empty-array<Tk, Tv>>
19+
*/
20+
function non_empty_dict(TypeInterface $key_type, TypeInterface $value_type): TypeInterface
21+
{
22+
return new Internal\NonEmptyDictType($key_type, $value_type);
23+
}
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\Tests\Unit\Type;
6+
7+
use Psl\Collection;
8+
use Psl\Dict;
9+
use Psl\Iter;
10+
use Psl\Str;
11+
use Psl\Type;
12+
use Psl\Vec;
13+
14+
/**
15+
* @extends TypeTest<non-empty-array<array-key, mixed>>
16+
*/
17+
final class NonEmptyDictTypeTest extends TypeTest
18+
{
19+
public function getType(): Type\TypeInterface
20+
{
21+
return Type\non_empty_dict(Type\int(), Type\int());
22+
}
23+
24+
public function getValidCoercions(): iterable
25+
{
26+
yield [
27+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
28+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
29+
];
30+
31+
yield [
32+
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
33+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
34+
];
35+
36+
yield [
37+
new Collection\Vector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
38+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
39+
];
40+
41+
yield [
42+
new Collection\Map([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
43+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
44+
];
45+
46+
yield [
47+
new Collection\Vector(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']),
48+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
49+
];
50+
51+
yield [
52+
new Collection\Map(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']),
53+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
54+
];
55+
56+
yield [
57+
Dict\map_keys(Vec\range(1, 10), static fn(int $key): string => (string)$key),
58+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
59+
];
60+
61+
yield [
62+
Dict\map(
63+
Vec\range(1, 10),
64+
static fn(int $value): string => Str\format('00%d', $value)
65+
),
66+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
67+
];
68+
69+
yield [
70+
Dict\map_keys(
71+
Dict\map(
72+
Vec\range(1, 10),
73+
static fn(int $value): string => Str\format('00%d', $value)
74+
),
75+
static fn(int $key): string => Str\format('00%d', $key)
76+
),
77+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
78+
];
79+
}
80+
81+
public function getInvalidCoercions(): iterable
82+
{
83+
yield [[]];
84+
yield [1.0];
85+
yield [1.23];
86+
yield [Type\bool()];
87+
yield [null];
88+
yield [false];
89+
yield [true];
90+
yield [STDIN];
91+
}
92+
93+
public function getToStringExamples(): iterable
94+
{
95+
yield [$this->getType(), 'non-empty-dict<int, int>'];
96+
yield [Type\non_empty_dict(Type\array_key(), Type\int()), 'non-empty-dict<array-key, int>'];
97+
yield [Type\non_empty_dict(Type\array_key(), Type\string()), 'non-empty-dict<array-key, string>'];
98+
yield [
99+
Type\non_empty_dict(Type\array_key(), Type\object(Iter\Iterator::class)),
100+
'non-empty-dict<array-key, Psl\Iter\Iterator>'
101+
];
102+
}
103+
}

0 commit comments

Comments
 (0)