Skip to content

Commit

Permalink
feat(type): add mixed_dict, and mixed_vec types (#362)
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
BackEndTea committed Nov 4, 2022
1 parent d1f0c05 commit 66e5351
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 1 deletion.
2 changes: 2 additions & 0 deletions docs/component/type.md
Expand Up @@ -25,6 +25,8 @@
- [literal_scalar](./../../src/Psl/Type/literal_scalar.php#L14)
- [map](./../../src/Psl/Type/map.php#L18)
- [mixed](./../../src/Psl/Type/mixed.php#L10)
- [mixed_dict](./../../src/Psl/Type/mixed_dict.php#L10)
- [mixed_vec](./../../src/Psl/Type/mixed_vec.php#L10)
- [mutable_map](./../../src/Psl/Type/mutable_map.php#L18)
- [mutable_vector](./../../src/Psl/Type/mutable_vector.php#L16)
- [non_empty_dict](./../../src/Psl/Type/non_empty_dict.php#L16)
Expand Down
2 changes: 2 additions & 0 deletions src/Psl/Internal/Loader.php
Expand Up @@ -307,6 +307,8 @@ final class Loader
'Psl\\Type\\intersection' => 'Psl/Type/intersection.php',
'Psl\\Type\\iterable' => 'Psl/Type/iterable.php',
'Psl\\Type\\mixed' => 'Psl/Type/mixed.php',
'Psl\\Type\\mixed_dict' => 'Psl/Type/mixed_dict.php',
'Psl\\Type\\mixed_vec' => 'Psl/Type/mixed_vec.php',
'Psl\\Type\\null' => 'Psl/Type/null.php',
'Psl\\Type\\nullable' => 'Psl/Type/nullable.php',
'Psl\\Type\\optional' => 'Psl/Type/optional.php',
Expand Down
71 changes: 71 additions & 0 deletions src/Psl/Type/Internal/MixedDictType.php
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Internal;

use Psl\Type;
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;

/**
* @extends Type\Type<array<array-key, mixed>>
*
* @internal
*/
final class MixedDictType extends Type\Type
{
/**
* @throws CoercionException
*
* @return array<array-key, mixed>
*/
public function coerce(mixed $value): array
{
if (! is_iterable($value)) {
throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

if (is_array($value)) {
return $value;
}

$result = [];

$key_type = Type\array_key();
$key_type = $key_type->withTrace($this->getTrace()->withFrame('dict<' . $key_type->toString() . ', _>'));

/**
* @var array-key $k
* @var mixed $v
*
* @psalm-suppress MixedAssignment
*/
foreach ($value as $k => $v) {
$result[$key_type->coerce($k)] = $v;
}

return $result;
}

/**
* @throws AssertException
*
* @return array<array-key, mixed>
*
* @psalm-assert array<array-key, mixed> $value
*/
public function assert(mixed $value): array
{
if (! is_array($value)) {
throw AssertException::withValue($value, $this->toString(), $this->getTrace());
}

return $value;
}

public function toString(): string
{
return 'dict<array-key, mixed>';
}
}
82 changes: 82 additions & 0 deletions src/Psl/Type/Internal/MixedVecType.php
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Internal;

use Psl\Type;
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;

/**
* @extends Type\Type<list<mixed>>
*
* @internal
*/
final class MixedVecType extends Type\Type
{
/**
* @psalm-assert-if-true list<Tv> $value
*/
public function matches(mixed $value): bool
{
return is_array($value) && array_is_list($value);
}

/**
* @throws CoercionException
*
* @return list<mixed>
*/
public function coerce(mixed $value): iterable
{
if (! is_iterable($value)) {
throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

if (is_array($value)) {
if (! array_is_list($value)) {
return array_values($value);
}

return $value;
}

/**
* @var list<mixed> $entries
*/
$result = [];

/**
* @var mixed $v
*
* @psalm-suppress MixedAssignment
*/
foreach ($value as $v) {
$result[] = $v;
}

return $result;
}

/**
* @throws AssertException
*
* @return list<mixed>
*
* @psalm-assert list<mixed> $value
*/
public function assert(mixed $value): array
{
if (! is_array($value) || ! array_is_list($value)) {
throw AssertException::withValue($value, $this->toString(), $this->getTrace());
}

return $value;
}

public function toString(): string
{
return 'vec<mixed>';
}
}
13 changes: 13 additions & 0 deletions src/Psl/Type/mixed_dict.php
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Psl\Type;

/**
* @return TypeInterface<array<array-key, mixed>>
*/
function mixed_dict(): TypeInterface
{
return new Internal\MixedDictType();
}
13 changes: 13 additions & 0 deletions src/Psl/Type/mixed_vec.php
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Psl\Type;

/**
* @return TypeInterface<list<mixed>>
*/
function mixed_vec(): TypeInterface
{
return new Internal\MixedVecType();
}
89 changes: 89 additions & 0 deletions tests/unit/Type/MixedDictTypeTest.php
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Unit\Type;

use Generator;
use Psl\Collection;
use Psl\Dict;
use Psl\Str;
use Psl\Type;
use Psl\Vec;
use SplObjectStorage;
use stdClass;

final class MixedDictTypeTest extends TypeTest
{
public function getType(): Type\TypeInterface
{
return Type\mixed_dict();
}

public function getValidCoercions(): iterable
{
yield [
[],
[]
];

yield [
['foo' => 'bar'],
['foo' => 'bar']
];

$object = new stdClass();
yield [[0,1,2, 'foo' => 'bar', [], $object], [0,1,2, 'foo' => 'bar', [], $object]];

$gen = $this->generator();
yield [$gen, [1,2, 'asdf' => 'key']];

yield [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
];

yield [
new Collection\Vector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
];

yield [
new Collection\Map([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
];

yield [
Dict\map(
Vec\range(1, 10),
static fn(int $value): string => Str\format('00%d', $value)
),
['001', '002', '003', '004', '005', '006', '007', '008', '009', '0010']
];

$spl = new SplObjectStorage();
$spl[$object] = 'test';
yield [$spl, [$object]];
}

private function generator(): Generator
{
yield 1;
yield 2;
yield 'asdf' => 'key';
}

public function getInvalidCoercions(): iterable
{
yield [1];
yield [new stdClass()];
yield ['asdf'];
yield [false];
yield [null];
}

public function getToStringExamples(): iterable
{
yield [$this->getType(), 'dict<array-key, mixed>'];
}
}
92 changes: 92 additions & 0 deletions tests/unit/Type/MixedVecTypeTest.php
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Unit\Type;

use Psl\Collection;
use Psl\Dict;
use Psl\Str;
use Psl\Type;
use Psl\Vec;

final class MixedVecTypeTest extends TypeTest
{
public function getType(): Type\TypeInterface
{
return Type\mixed_vec();
}

public function getValidCoercions(): iterable
{
yield [
[],
[]
];
yield [
['foo' => 'bar'],
['bar']
];

yield [
[1,2,3],
[1,2,3]
];

yield [
[16 => ['arr'], 'foo', 'bar', 45 => 1, 44 => 2],
[['arr'], 'foo', 'bar', 1,2]
];

yield [
new Collection\Vector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
];

yield [
new Collection\Map([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
];

yield [
new Collection\Vector(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']),
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
];

yield [
new Collection\Map([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
];

yield [
Dict\map_keys(Vec\range(1, 10), static fn(int $key): string => (string)$key),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
];

yield [
Dict\map(Vec\range(1, 10), static fn(int $value): string => Str\format('00%d', $value)),
['001', '002', '003', '004', '005', '006', '007', '008', '009', '0010']
];

yield [
['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5],
[1, 2, 3, 4, 5]
];
}

public function getInvalidCoercions(): iterable
{
yield [1.0];
yield [1.23];
yield [Type\bool()];
yield [null];
yield [false];
yield [true];
yield [STDIN];
}

public function getToStringExamples(): iterable
{
yield [$this->getType(), 'vec<mixed>'];
}
}
6 changes: 5 additions & 1 deletion tests/unit/Type/TypeTest.php
Expand Up @@ -119,7 +119,11 @@ public function testInvalidCoercion($value): void
{
$this->expectException(CoercionException::class);

$this->getType()->coerce($value);
try {
$ret = $this->getType()->coerce($value);
} catch (CoercionException $e) {
throw $e;
}
}

/**
Expand Down

0 comments on commit 66e5351

Please sign in to comment.