Skip to content

Commit

Permalink
feat: allow mapping to array-key type
Browse files Browse the repository at this point in the history
  • Loading branch information
romm committed Mar 27, 2024
1 parent 2b21ed4 commit c1eeb86
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 35 deletions.
4 changes: 2 additions & 2 deletions src/Type/CombiningType.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface CombiningType extends CompositeType
public function isMatchedBy(Type $other): bool;

/**
* @return Type[]
* @return non-empty-list<Type>
*/
public function types(): iterable;
public function types(): array;
}
3 changes: 2 additions & 1 deletion src/Type/Parser/Lexer/Token/ClassNameToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use function array_keys;
use function array_map;
use function array_shift;
use function array_values;
use function count;
use function explode;

Expand Down Expand Up @@ -101,7 +102,7 @@ private function classConstant(TokenStream $stream): ?Type
$cases = array_map(static fn ($value) => ValueTypeFactory::from($value), $cases);

if (count($cases) > 1) {
return new UnionType(...$cases);
return new UnionType(...array_values($cases));
}

return reset($cases);
Expand Down
56 changes: 49 additions & 7 deletions src/Type/Types/ArrayKeyType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,56 @@

namespace CuyZ\Valinor\Type\Types;

use CuyZ\Valinor\Type\CombiningType;
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
use CuyZ\Valinor\Type\IntegerType;
use CuyZ\Valinor\Type\Parser\Exception\Iterable\InvalidArrayKey;
use CuyZ\Valinor\Type\ScalarType;
use CuyZ\Valinor\Type\StringType;
use CuyZ\Valinor\Type\Type;
use LogicException;

use function is_int;

/** @internal */
final class ArrayKeyType implements Type
final class ArrayKeyType implements ScalarType
{
private static self $default;

private static self $integer;

private static self $string;

/** @var array<Type> */
/** @var non-empty-list<IntegerType|StringType> */
private array $types;

private string $signature;

private function __construct(Type $type)
{
$this->signature = $type->toString();
$this->types = $type instanceof CombiningType
$types = $type instanceof UnionType
? [...$type->types()]
: [$type];

foreach ($this->types as $subType) {
foreach ($types as $subType) {
if (! $subType instanceof IntegerType && ! $subType instanceof StringType) {
throw new InvalidArrayKey($subType);
}
}

/** @var non-empty-list<IntegerType|StringType> $types */
$this->types = $types;
$this->signature = $type->toString();
}

public static function default(): self
{
return self::$default ??= new self(new UnionType(NativeIntegerType::get(), NativeStringType::get()));
if (!isset(self::$default)) {
self::$default = new self(new UnionType(NativeIntegerType::get(), NativeStringType::get()));
self::$default->signature = 'array-key';
}

return self::$default;
}

public static function integer(): self
Expand Down Expand Up @@ -86,6 +97,10 @@ public function matches(Type $other): bool
return true;
}

if ($other instanceof UnionType) {
return $this->isMatchedBy($other);
}

if (! $other instanceof self) {
return false;
}
Expand Down Expand Up @@ -114,6 +129,33 @@ public function isMatchedBy(Type $other): bool
return false;
}

public function canCast(mixed $value): bool
{
foreach ($this->types as $type) {
if ($type->canCast($value)) {
return true;
}
}

return false;
}

public function cast(mixed $value): string|int
{
foreach ($this->types as $type) {
if ($type->canCast($value)) {
return $type->cast($value);
}
}

throw new LogicException();
}

public function errorMessage(): ErrorMessage
{
return MessageBuilder::newError('Value {source_value} is not a valid array key.')->build();
}

public function toString(): string
{
return $this->signature;
Expand Down
11 changes: 6 additions & 5 deletions src/Type/Types/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;

use function array_values;
use function implode;

/** @internal */
final class IntersectionType implements CombiningType
{
/** @var ObjectType[] */
/** @var non-empty-list<ObjectType> */
private array $types;

private string $signature;

public function __construct(ObjectType ...$types)
public function __construct(ObjectType $type, ObjectType $otherType, ObjectType ...$otherTypes)
{
$this->types = $types;
$this->signature = implode('&', array_map(fn (Type $type) => $type->toString(), $types));
$this->types = [$type, $otherType, ...array_values($otherTypes)];
$this->signature = implode('&', array_map(fn (Type $type) => $type->toString(), $this->types));
}

public function accepts(mixed $value): bool
Expand Down Expand Up @@ -82,7 +83,7 @@ public function traverse(): array
}

/**
* @return ObjectType[]
* @return non-empty-list<ObjectType>
*/
public function types(): array
{
Expand Down
30 changes: 17 additions & 13 deletions src/Type/Types/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,34 @@
/** @internal */
final class UnionType implements CombiningType
{
/** @var Type[] */
private array $types = [];
/** @var non-empty-list<Type> */
private array $types;

private string $signature;

public function __construct(Type ...$types)
public function __construct(Type $type, Type $otherType, Type ...$otherTypes)
{
$this->signature = implode('|', array_map(fn (Type $type) => $type->toString(), $types));
$types = [$type, $otherType, ...$otherTypes];
$filteredTypes = [];

foreach ($types as $type) {
if ($type instanceof self) {
foreach ($type->types as $subType) {
$this->types[] = $subType;
foreach ($types as $subType) {
if ($subType instanceof self) {
foreach ($subType->types as $anotherSubType) {
$filteredTypes[] = $anotherSubType;
}

continue;
}

if ($type instanceof MixedType) {
if ($subType instanceof MixedType) {
throw new ForbiddenMixedType();
}

$this->types[] = $type;
$filteredTypes[] = $subType;
}

$this->types = $filteredTypes;
$this->signature = implode('|', array_map(fn (Type $type) => $type->toString(), $this->types));
}

public function accepts(mixed $value): bool
Expand All @@ -65,12 +69,12 @@ public function matches(Type $other): bool
}

foreach ($this->types as $type) {
if (! $type->matches($other)) {
return false;
if ($type->matches($other)) {
return true;
}
}

return true;
return false;
}

public function isMatchedBy(Type $other): bool
Expand Down
2 changes: 1 addition & 1 deletion tests/Functional/Type/Parser/Lexer/GenericLexerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public function test_generic_with_non_matching_array_key_type_for_template_throw

$this->expectException(InvalidAssignedGeneric::class);
$this->expectExceptionCode(1604613633);
$this->expectExceptionMessage("The generic `bool` is not a subtype of `int|string` for the template `Template` of the class `$className`.");
$this->expectExceptionMessage("The generic `bool` is not a subtype of `array-key` for the template `Template` of the class `$className`.");

$this->parser->parse("$className<bool>");
}
Expand Down
29 changes: 28 additions & 1 deletion tests/Integration/Mapping/Object/ScalarValuesMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public function test_values_are_mapped_properly(): void
'classString' => self::class,
'classStringOfDateTime' => DateTimeImmutable::class,
'classStringOfAlias' => stdClass::class,
'arrayKeyWithString' => 'foo',
'arrayKeyWithInteger' => 42,
];

foreach ([ScalarValues::class, ScalarValuesWithConstructor::class] as $class) {
Expand Down Expand Up @@ -81,6 +83,8 @@ public function test_values_are_mapped_properly(): void
self::assertSame(self::class, $result->classString);
self::assertSame(DateTimeImmutable::class, $result->classStringOfDateTime);
self::assertSame(stdClass::class, $result->classStringOfAlias);
self::assertSame('foo', $result->arrayKeyWithString);
self::assertSame(42, $result->arrayKeyWithInteger);
}
}

Expand All @@ -94,6 +98,17 @@ public function test_value_with_invalid_type_throws_exception(): void
self::assertSame('Value object(stdClass) is not a valid string.', (string)$error);
}
}

public function test_invalid_array_key_throws_exception(): void
{
try {
$this->mapperBuilder()->mapper()->map('array-key', new stdClass());
} catch (MappingError $exception) {
$error = $exception->node()->messages()[0];

self::assertSame('Value object(stdClass) is not a valid array key.', (string)$error);
}
}
}

class ScalarValues
Expand Down Expand Up @@ -173,6 +188,12 @@ class ScalarValues

/** @var class-string<ObjectAlias> */
public string $classStringOfAlias;

/** @var array-key */
public string|int $arrayKeyWithString;

/** @var array-key */
public string|int $arrayKeyWithInteger;
}

class ScalarValuesWithConstructor extends ScalarValues
Expand Down Expand Up @@ -200,6 +221,8 @@ class ScalarValuesWithConstructor extends ScalarValues
* @param class-string $classString
* @param class-string<DateTimeInterface> $classStringOfDateTime
* @param class-string<ObjectAlias> $classStringOfAlias
* @param array-key $arrayKeyWithString
* @param array-key $arrayKeyWithInteger
*/
public function __construct(
bool $boolean,
Expand Down Expand Up @@ -228,7 +251,9 @@ public function __construct(
string $stringValueContainingSpecialCharsWithDoubleQuote,
string $classString,
string $classStringOfDateTime,
string $classStringOfAlias
string $classStringOfAlias,
int|string $arrayKeyWithString,
int|string $arrayKeyWithInteger,
) {
$this->boolean = $boolean;
$this->float = $float;
Expand Down Expand Up @@ -257,5 +282,7 @@ public function __construct(
$this->classString = $classString;
$this->classStringOfDateTime = $classStringOfDateTime;
$this->classStringOfAlias = $classStringOfAlias;
$this->arrayKeyWithString = $arrayKeyWithString;
$this->arrayKeyWithInteger = $arrayKeyWithInteger;
}
}
11 changes: 11 additions & 0 deletions tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,17 @@ public function test_source_value_is_casted_when_other_type_cannot_be_casted():

self::assertSame('foo', $result);
}

public function test_source_can_be_casted_to_array_key(): void
{
try {
$result = $this->mapper->map('array-key', new StringableObject('foo'));
} catch (MappingError $error) {
$this->mappingFail($error);
}

self::assertSame('foo', $result);
}
}

interface SomeInterfaceForClassWithNoProperties {}
Expand Down
Loading

0 comments on commit c1eeb86

Please sign in to comment.