diff --git a/src/Type/Parser/Exception/Enum/NotBackedEnum.php b/src/Type/Parser/Exception/Enum/NotBackedEnum.php new file mode 100644 index 00000000..61e6d515 --- /dev/null +++ b/src/Type/Parser/Exception/Enum/NotBackedEnum.php @@ -0,0 +1,20 @@ +`.", + 1618994728 + ); + } +} diff --git a/src/Type/Parser/Exception/Magic/NonArrayOf.php b/src/Type/Parser/Exception/Magic/NonArrayOf.php new file mode 100644 index 00000000..51d87ed5 --- /dev/null +++ b/src/Type/Parser/Exception/Magic/NonArrayOf.php @@ -0,0 +1,21 @@ +toString()}>` is not an array.", + 1618994728 + ); + } +} diff --git a/src/Type/Parser/Exception/Magic/OpeningBracketMissing.php b/src/Type/Parser/Exception/Magic/OpeningBracketMissing.php new file mode 100644 index 00000000..6c0d3a3b --- /dev/null +++ b/src/Type/Parser/Exception/Magic/OpeningBracketMissing.php @@ -0,0 +1,20 @@ +`.", + 1618994728 + ); + } +} diff --git a/src/Type/Parser/Lexer/NativeLexer.php b/src/Type/Parser/Lexer/NativeLexer.php index 6f6172d3..8801cc7d 100644 --- a/src/Type/Parser/Lexer/NativeLexer.php +++ b/src/Type/Parser/Lexer/NativeLexer.php @@ -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; @@ -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()), diff --git a/src/Type/Parser/Lexer/Token/ValueOfToken.php b/src/Type/Parser/Lexer/Token/ValueOfToken.php new file mode 100644 index 00000000..e727283d --- /dev/null +++ b/src/Type/Parser/Lexer/Token/ValueOfToken.php @@ -0,0 +1,63 @@ +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) { + $subType = $subType->types()[0]; + } + + if (! $subType instanceof EnumType) { + throw new NotBackedEnum($subType->toString()); + } + + $list = []; + foreach ($subType->cases() as $case) { + if (! $case instanceof BackedEnum) { + 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'; + } +} diff --git a/tests/Functional/Type/Parser/LexingParserTest.php b/tests/Functional/Type/Parser/LexingParserTest.php index db84ab9e..e5ce2da5 100644 --- a/tests/Functional/Type/Parser/LexingParserTest.php +++ b/tests/Functional/Type/Parser/LexingParserTest.php @@ -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' => [ + 'raw' => "value-of<" . BackedStringEnum::class . ">", + 'transformed' => "'foo'|'bar'|'baz'", + 'type' => UnionType::class, + ]; + yield 'value-of' => [ + 'raw' => "value-of<" . BackedIntegerEnum::class . ">", + 'transformed' => "42|404|1337", + 'type' => UnionType::class, + ]; } public function test_multiple_union_types_are_parsed(): void diff --git a/tests/Integration/Mapping/EnumValueOfMappingTest.php b/tests/Integration/Mapping/EnumValueOfMappingTest.php new file mode 100644 index 00000000..830b110b --- /dev/null +++ b/tests/Integration/Mapping/EnumValueOfMappingTest.php @@ -0,0 +1,79 @@ +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, 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, 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; + +}