-
-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for
value-of<BackedEnum>
type
This type can be used as follows: ```php enum Suit: string { case Hearts = 'H'; case Diamonds = 'D'; case Clubs = 'C'; case Spades = 'S'; } $suit = (new \CuyZ\Valinor\MapperBuilder()) ->mapper() ->map(Suit::class, 'D'); // $suit === Suit::Diamonds ```
- Loading branch information
Showing
8 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
src/Type/Parser/Exception/Magic/ValueOfClosingBracketMissing.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ValueOfClosingBracketMissing extends RuntimeException implements InvalidType | ||
{ | ||
public function __construct(Type $type) | ||
{ | ||
parent::__construct( | ||
"The closing bracket is missing for `value-of<{$type->toString()}>`.", | ||
1717702289 | ||
); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/Type/Parser/Exception/Magic/ValueOfIncorrectSubType.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ValueOfIncorrectSubType extends RuntimeException implements InvalidType | ||
{ | ||
public function __construct(Type $type) | ||
{ | ||
parent::__construct( | ||
"Invalid subtype `value-of<{$type->toString()}>`, it should be a `BackedEnum`.", | ||
1717702683 | ||
); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/Type/Parser/Exception/Magic/ValueOfOpeningBracketMissing.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ValueOfOpeningBracketMissing extends RuntimeException implements InvalidType | ||
{ | ||
public function __construct() | ||
{ | ||
parent::__construct( | ||
"The opening bracket is missing for `value-of<...>`.", | ||
1717702268 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace CuyZ\Valinor\Type\Parser\Lexer\Token; | ||
|
||
use BackedEnum; | ||
use CuyZ\Valinor\Type\Parser\Exception\Magic\ValueOfIncorrectSubType; | ||
use CuyZ\Valinor\Type\Parser\Exception\Magic\ValueOfClosingBracketMissing; | ||
use CuyZ\Valinor\Type\Parser\Exception\Magic\ValueOfOpeningBracketMissing; | ||
use CuyZ\Valinor\Type\Parser\Lexer\TokenStream; | ||
use CuyZ\Valinor\Type\Type; | ||
use CuyZ\Valinor\Type\Types\EnumType; | ||
use CuyZ\Valinor\Type\Types\Factory\ValueTypeFactory; | ||
use CuyZ\Valinor\Type\Types\UnionType; | ||
use CuyZ\Valinor\Utility\IsSingleton; | ||
|
||
use function array_map; | ||
use function array_values; | ||
use function count; | ||
use function is_a; | ||
|
||
/** @internal */ | ||
final class ValueOfToken implements TraversingToken | ||
{ | ||
use IsSingleton; | ||
|
||
public function traverse(TokenStream $stream): Type | ||
{ | ||
if ($stream->done() || !$stream->forward() instanceof OpeningBracketToken) { | ||
throw new ValueOfOpeningBracketMissing(); | ||
} | ||
|
||
$subType = $stream->read(); | ||
|
||
if ($stream->done() || !$stream->forward() instanceof ClosingBracketToken) { | ||
throw new ValueOfClosingBracketMissing($subType); | ||
} | ||
|
||
if (! $subType instanceof EnumType) { | ||
throw new ValueOfIncorrectSubType($subType); | ||
} | ||
|
||
if (! is_a($subType->className(), BackedEnum::class, true)) { | ||
throw new ValueOfIncorrectSubType($subType); | ||
} | ||
|
||
$cases = array_map( | ||
// @phpstan-ignore-next-line / We know it's a BackedEnum | ||
fn (BackedEnum $case) => ValueTypeFactory::from($case->value), | ||
array_values($subType->cases()), | ||
); | ||
|
||
if (count($cases) > 1) { | ||
return new UnionType(...$cases); | ||
} | ||
|
||
return $cases[0]; | ||
} | ||
|
||
public function symbol(): string | ||
{ | ||
return 'value-of'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace CuyZ\Valinor\Tests\Integration\Mapping; | ||
|
||
use CuyZ\Valinor\Mapper\MappingError; | ||
use CuyZ\Valinor\Tests\Integration\IntegrationTestCase; | ||
|
||
final class EnumValueOfMappingTest extends IntegrationTestCase | ||
{ | ||
public function test_can_map_value_of_string_enum(): 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_value_of_enum_with_one_case(): void | ||
{ | ||
try { | ||
$result = $this->mapperBuilder() | ||
->mapper() | ||
->map('value-of<' . SomeStringEnumForValueOfWithOneCase::class . '>', SomeStringEnumForValueOf::FOO->value); | ||
} catch (MappingError $error) { | ||
$this->mappingFail($error); | ||
} | ||
|
||
self::assertSame(SomeStringEnumForValueOf::FOO->value, $result); // @phpstan-ignore-line | ||
} | ||
|
||
public function test_can_map_value_of_integer_enum(): 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 SomeStringEnumForValueOfWithOneCase: string | ||
{ | ||
case FOO = 'FOO'; | ||
} | ||
|
||
enum SomeIntegerEnumForValueOf: int | ||
{ | ||
case FOO = 42; | ||
case FOZ = 404; | ||
case BAZ = 1337; | ||
} |