Skip to content

Commit

Permalink
Add support for value-of<BackedEnum>
Browse files Browse the repository at this point in the history
  • Loading branch information
robchett committed May 3, 2024
1 parent 40e6fa3 commit 68d1027
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 0 deletions.
20 changes: 20 additions & 0 deletions src/Type/Parser/Exception/Enum/NotBackedEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Enum;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;

/** @internal */
final class NotBackedEnum extends RuntimeException implements InvalidType
{
public function __construct(string $enumName)
{
parent::__construct(
"`$enumName` is not BackedEnum.",
1653468439
);
}
}
20 changes: 20 additions & 0 deletions src/Type/Parser/Exception/Magic/ClosingBracketMissing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Magic;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;

/** @internal */
final class ClosingBracketMissing extends RuntimeException implements InvalidType
{
public function __construct(string $symbol)
{
parent::__construct(
"The closing bracket is missing for `$symbol<...>`.",
1618994728
);
}
}
21 changes: 21 additions & 0 deletions src/Type/Parser/Exception/Magic/NonArrayOf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Magic;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Type;
use RuntimeException;

/** @internal */
final class NonArrayOf extends RuntimeException implements InvalidType
{
public function __construct(string $symbol, Type $type)
{
parent::__construct(
"The type inside of `$symbol<{$type->toString()}>` is not an array.",
1618994728
);
}
}
20 changes: 20 additions & 0 deletions src/Type/Parser/Exception/Magic/OpeningBracketMissing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Magic;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;

/** @internal */
final class OpeningBracketMissing extends RuntimeException implements InvalidType
{
public function __construct(string $symbol)
{
parent::__construct(
"The opening bracket is missing for `$symbol<...>`.",
1618994728
);
}
}
2 changes: 2 additions & 0 deletions src/Type/Parser/Lexer/NativeLexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use CuyZ\Valinor\Type\Parser\Lexer\Token\IterableToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ListToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\NullableToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ValueOfToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningCurlyBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningSquareBracketToken;
Expand Down Expand Up @@ -78,6 +79,7 @@ public function tokenize(string $symbol): Token
'iterable' => IterableToken::get(),
'class-string' => ClassStringToken::get(),
'callable' => CallableToken::get(),
'value-of' => ValueOfToken::get(),

'null' => new TypeToken(NullType::get()),
'true' => new TypeToken(BooleanValueType::true()),
Expand Down
63 changes: 63 additions & 0 deletions src/Type/Parser/Lexer/Token/ValueOfToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Lexer\Token;

use BackedEnum;
use CuyZ\Valinor\Type\Parser\Exception\Enum\NotBackedEnum;
use CuyZ\Valinor\Type\Parser\Exception\Magic\ClosingBracketMissing;
use CuyZ\Valinor\Type\Parser\Exception\Magic\OpeningBracketMissing;
use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\EnumType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use CuyZ\Valinor\Type\Types\StringValueType;
use CuyZ\Valinor\Type\Types\UnionType;
use CuyZ\Valinor\Utility\IsSingleton;

/** @internal */
final class ValueOfToken implements TraversingToken
{
use IsSingleton;

public function traverse(TokenStream $stream): Type
{
if ($stream->done() || !$stream->forward() instanceof OpeningBracketToken) {
throw new OpeningBracketMissing($this->symbol());
}

$subType = $stream->read();

if ($stream->done() || !$stream->forward() instanceof ClosingBracketToken) {
throw new ClosingBracketMissing($this->symbol());
}

if ($subType instanceof UnionType && count($subType->types()) === 1) {

Check warning on line 36 in src/Type/Parser/Lexer/Token/ValueOfToken.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "InstanceOf_": --- Original +++ New @@ @@ if ($stream->done() || !$stream->forward() instanceof ClosingBracketToken) { throw new ClosingBracketMissing($this->symbol()); } - if ($subType instanceof UnionType && count($subType->types()) === 1) { + if (false && count($subType->types()) === 1) { $subType = $subType->types()[0]; } if (!$subType instanceof EnumType) {

Check warning on line 36 in src/Type/Parser/Lexer/Token/ValueOfToken.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ if ($stream->done() || !$stream->forward() instanceof ClosingBracketToken) { throw new ClosingBracketMissing($this->symbol()); } - if ($subType instanceof UnionType && count($subType->types()) === 1) { + if ($subType instanceof UnionType && count($subType->types()) === 2) { $subType = $subType->types()[0]; } if (!$subType instanceof EnumType) {

Check warning on line 36 in src/Type/Parser/Lexer/Token/ValueOfToken.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "Identical": --- Original +++ New @@ @@ if ($stream->done() || !$stream->forward() instanceof ClosingBracketToken) { throw new ClosingBracketMissing($this->symbol()); } - if ($subType instanceof UnionType && count($subType->types()) === 1) { + if ($subType instanceof UnionType && count($subType->types()) !== 1) { $subType = $subType->types()[0]; } if (!$subType instanceof EnumType) {
$subType = $subType->types()[0];
}

if (! $subType instanceof EnumType) {

Check warning on line 40 in src/Type/Parser/Lexer/Token/ValueOfToken.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "InstanceOf_": --- Original +++ New @@ @@ if ($subType instanceof UnionType && count($subType->types()) === 1) { $subType = $subType->types()[0]; } - if (!$subType instanceof EnumType) { + if (!true) { throw new NotBackedEnum($subType->toString()); } $list = [];
throw new NotBackedEnum($subType->toString());
}

$list = [];
foreach ($subType->cases() as $case) {
if (! $case instanceof BackedEnum) {

Check warning on line 46 in src/Type/Parser/Lexer/Token/ValueOfToken.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "InstanceOf_": --- Original +++ New @@ @@ } $list = []; foreach ($subType->cases() as $case) { - if (!$case instanceof BackedEnum) { + if (!true) { throw new NotBackedEnum($this->symbol()); } if (is_string($case->value)) {
throw new NotBackedEnum($this->symbol());
}
if (is_string($case->value)) {
$list[] = StringValueType::from("'$case->value'");
} else {
$list[] = new IntegerValueType($case->value);
}
}

return new UnionType(...$list);
}

public function symbol(): string
{
return 'value-of';
}
}
10 changes: 10 additions & 0 deletions tests/Functional/Type/Parser/LexingParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,16 @@ public static function parse_valid_types_returns_valid_result_data_provider(): i
'transformed' => PureEnum::class . '::*A*',
'type' => EnumType::class,
];
yield 'value-of<BackedStringEnum>' => [
'raw' => "value-of<" . BackedStringEnum::class . ">",
'transformed' => "'foo'|'bar'|'baz'",
'type' => UnionType::class,
];
yield 'value-of<BackedIntegerEnum>' => [
'raw' => "value-of<" . BackedIntegerEnum::class . ">",
'transformed' => "42|404|1337",
'type' => UnionType::class,
];
}

public function test_multiple_union_types_are_parsed(): void
Expand Down
79 changes: 79 additions & 0 deletions tests/Integration/Mapping/EnumValueOfMappingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Tests\Integration\Mapping;

use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Mapper\Object\Constructor;
use CuyZ\Valinor\Tests\Integration\IntegrationTestCase;

final class EnumValueOfMappingTest extends IntegrationTestCase
{
public function test_can_map_string_enum_value_of(): void
{
try {
$result = $this->mapperBuilder()
->mapper()
->map('value-of<' . SomeStringEnumForValueOf::class . '>', SomeStringEnumForValueOf::FOO->value);
} catch (MappingError $error) {
$this->mappingFail($error);
}

self::assertSame(SomeStringEnumForValueOf::FOO->value, $result);
}

public function test_can_map_integer_enum_value_of(): void
{
try {
$result = $this->mapperBuilder()
->mapper()
->map('value-of<' . SomeIntegerEnumForValueOf::class . '>', SomeIntegerEnumForValueOf::FOO->value);
} catch (MappingError $error) {
$this->mappingFail($error);
}

self::assertSame(SomeIntegerEnumForValueOf::FOO->value, $result);
}

public function test_array_keys_using_value_of(): void
{
try {
$result = $this->mapperBuilder()
->mapper()
->map('array<value-of<' . SomeStringEnumForValueOf::class . '>, string>', [SomeStringEnumForValueOf::FOO->value => 'foo']);
} catch (MappingError $error) {
$this->mappingFail($error);
}

self::assertSame([SomeStringEnumForValueOf::FOO->value => 'foo'], $result);
}


public function test_array_keys_using_value_of_error(): void
{
try {
$this->mapperBuilder()
->mapper()
->map('array<value-of<' . SomeStringEnumForValueOf::class . '>, string>', ['oof' => 'foo']);
} catch (MappingError $exception) {
$error = $exception->node()->children()['oof']->messages()[0];
self::assertSame("Key 'oof' does not match type `'FOO'|'FOZ'|'BAZ'`.", (string)$error);
}
}
}

enum SomeStringEnumForValueOf: string
{
case FOO = 'FOO';
case FOZ = 'FOZ';
case BAZ = 'BAZ';

}
enum SomeIntegerEnumForValueOf: int
{
case FOO = 42;
case FOZ = 404;
case BAZ = 1337;

}

0 comments on commit 68d1027

Please sign in to comment.