From 2393aa6ab6cd4c0168d4de81088ffa75833e4aee Mon Sep 17 00:00:00 2001 From: Neghmurken Date: Fri, 28 Oct 2022 16:12:44 +0200 Subject: [PATCH] Add enumExists and enumCase assertion --- README.md | 4 +- lib/Assert/Assertion.php | 78 +++++++++++++++++++ lib/Assert/AssertionChain.php | 2 + lib/Assert/LazyAssertion.php | 2 + phpstan-tests.neon | 2 +- tests/Assert/Tests/AssertEnumTest.php | 68 ++++++++++++++++ .../Assert/Tests/Fixtures/TestBackedEnum.php | 20 +++++ tests/Assert/Tests/Fixtures/TestUnitEnum.php | 20 +++++ 8 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 tests/Assert/Tests/AssertEnumTest.php create mode 100644 tests/Assert/Tests/Fixtures/TestBackedEnum.php create mode 100644 tests/Assert/Tests/Fixtures/TestUnitEnum.php diff --git a/README.md b/README.md index 518156ca..36e9dc84 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ Assertion::directory(string $value); Assertion::e164(string $value); Assertion::email(mixed $value); Assertion::endsWith(mixed $string, string $needle); +Assertion::enumCase(mixed $value, string $enumClass); +Assertion::enumExists(mixed $value); Assertion::eq(mixed $value, mixed $value2); Assertion::eqArraySubset(mixed $value, mixed $value2); Assertion::extensionLoaded(mixed $value); @@ -219,7 +221,7 @@ Assertion::ipv6(string $value, int $flag = null); Assertion::isArray(mixed $value); Assertion::isArrayAccessible(mixed $value); Assertion::isCallable(mixed $value); -Assertion::isCountable(array|Countable|ResourceBundle|SimpleXMLElement $value); +Assertion::isCountable(mixed $value); Assertion::isInstanceOf(mixed $value, string $className); Assertion::isJsonString(mixed $value); Assertion::isObject(mixed $value); diff --git a/lib/Assert/Assertion.php b/lib/Assert/Assertion.php index 3c2f0d7c..2f9cd306 100644 --- a/lib/Assert/Assertion.php +++ b/lib/Assert/Assertion.php @@ -24,6 +24,7 @@ use SimpleXMLElement; use Throwable; use Traversable; +use ValueError; /** * Assert library. @@ -48,6 +49,8 @@ * @method static bool allE164(string[] $value, string|callable $message = null, string $propertyPath = null) Assert that the given string is a valid E164 Phone Number for all values. * @method static bool allEmail(mixed[] $value, string|callable $message = null, string $propertyPath = null) Assert that value is an email address (using input_filter/FILTER_VALIDATE_EMAIL) for all values. * @method static bool allEndsWith(mixed[] $string, string $needle, string|callable $message = null, string $propertyPath = null, string $encoding = 'utf8') Assert that string ends with a sequence of chars for all values. + * @method static bool allEnumCase(mixed[] $value, string $enumClass, string|callable $message = null, ?string $propertyPath = null) Assert that the value is a valid backed enum case for all values. + * @method static bool allEnumExists(mixed[] $value, string|callable $message = null, ?string $propertyPath = null) Assert that the enum exists for all values. * @method static bool allEq(mixed[] $value, mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that two values are equal (using ==) for all values. * @method static bool allEqArraySubset(mixed[] $value, mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that the array contains the subset for all values. * @method static bool allExtensionLoaded(mixed[] $value, string|callable $message = null, string $propertyPath = null) Assert that extension is loaded for all values. @@ -137,6 +140,8 @@ * @method static bool nullOrE164(string|null $value, string|callable $message = null, string $propertyPath = null) Assert that the given string is a valid E164 Phone Number or that the value is null. * @method static bool nullOrEmail(mixed|null $value, string|callable $message = null, string $propertyPath = null) Assert that value is an email address (using input_filter/FILTER_VALIDATE_EMAIL) or that the value is null. * @method static bool nullOrEndsWith(mixed|null $string, string $needle, string|callable $message = null, string $propertyPath = null, string $encoding = 'utf8') Assert that string ends with a sequence of chars or that the value is null. + * @method static bool nullOrEnumCase(mixed|null $value, string $enumClass, string|callable $message = null, ?string $propertyPath = null) Assert that the value is a valid backed enum case or that the value is null. + * @method static bool nullOrEnumExists(mixed|null $value, string|callable $message = null, ?string $propertyPath = null) Assert that the enum exists or that the value is null. * @method static bool nullOrEq(mixed|null $value, mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that two values are equal (using ==) or that the value is null. * @method static bool nullOrEqArraySubset(mixed|null $value, mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that the array contains the subset or that the value is null. * @method static bool nullOrExtensionLoaded(mixed|null $value, string|callable $message = null, string $propertyPath = null) Assert that extension is loaded or that the value is null. @@ -290,6 +295,8 @@ class Assertion const INVALID_MAX_COUNT = 228; const INVALID_STRING_NOT_CONTAINS = 229; const INVALID_UNIQUE_VALUES = 230; + const INVALID_ENUM = 231; + const INVALID_ENUM_CASE = 232; /** * Exception to throw when an assertion failed. @@ -2710,6 +2717,77 @@ public static function base64($value, $message = null, string $propertyPath = nu return true; } + /** + * Assert that the enum exists. + * + * @param mixed $value + * @param string|callable|null $message + * @param string|null $propertyPath + * + * @psalm-assert class-string $value + * + * @return bool + * + * @throws AssertionFailedException + */ + public static function enumExists($value, $message = null, string $propertyPath = null): bool + { + if (!function_exists('enum_exists')) { + throw new \BadMethodCallException('The "enum_exists" function is not available. Please install the "symfony/polyfill-php81" package.'); + } + + if (!\enum_exists($value)) { + $message = \sprintf( + static::generateMessage($message ?: 'Enum "%s" does not exist.'), + static::stringify($value) + ); + + throw static::createException($value, $message, static::INVALID_ENUM, $propertyPath); + } + + return true; + } + + /** + * Assert that the value is a valid backed enum case. + * + * @param mixed $value + * @param string|callable|null $message + * @param string|null $propertyPath + * + * @psalm-assert class-string $value + * + * @throws AssertionFailedException + */ + public static function enumCase($value, string $enumClass, $message = null, string $propertyPath = null): bool + { + static::enumExists($enumClass); + static::implementsInterface($enumClass, \BackedEnum::class); + + try { + $enumClass::from($value); + } catch (\ValueError) { + $message = \sprintf( + static::generateMessage($message ?: 'Value "%s" is not a valid backing value for enum "%s". Allowed : %s'), + static::stringify($value), + $enumClass, + \implode( + ', ', + \array_map( + function ($case) { + return self::stringify($case->value); + }, + $enumClass::cases() + ) + ), + ); + + throw static::createException($value, $message, static::INVALID_ENUM_CASE, $propertyPath); + } + + return true; + } + /** * Helper method that handles building the assertion failure exceptions. * They are returned from this method so that the stack trace still shows diff --git a/lib/Assert/AssertionChain.php b/lib/Assert/AssertionChain.php index 4c444350..bb49c5fd 100644 --- a/lib/Assert/AssertionChain.php +++ b/lib/Assert/AssertionChain.php @@ -39,6 +39,8 @@ * @method AssertionChain e164(string|callable $message = null, string $propertyPath = null) Assert that the given string is a valid E164 Phone Number. * @method AssertionChain email(string|callable $message = null, string $propertyPath = null) Assert that value is an email address (using input_filter/FILTER_VALIDATE_EMAIL). * @method AssertionChain endsWith(string $needle, string|callable $message = null, string $propertyPath = null, string $encoding = 'utf8') Assert that string ends with a sequence of chars. + * @method AssertionChain enumCase(string $enumClass, string|callable $message = null, ?string $propertyPath = null) Assert that the value is a valid backed enum case. + * @method AssertionChain enumExists(string|callable $message = null, ?string $propertyPath = null) Assert that the enum exists. * @method AssertionChain eq(mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that two values are equal (using ==). * @method AssertionChain eqArraySubset(mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that the array contains the subset. * @method AssertionChain extensionLoaded(string|callable $message = null, string $propertyPath = null) Assert that extension is loaded. diff --git a/lib/Assert/LazyAssertion.php b/lib/Assert/LazyAssertion.php index b3052178..854e67ff 100644 --- a/lib/Assert/LazyAssertion.php +++ b/lib/Assert/LazyAssertion.php @@ -39,6 +39,8 @@ * @method LazyAssertion e164(string|callable $message = null, string $propertyPath = null) Assert that the given string is a valid E164 Phone Number. * @method LazyAssertion email(string|callable $message = null, string $propertyPath = null) Assert that value is an email address (using input_filter/FILTER_VALIDATE_EMAIL). * @method LazyAssertion endsWith(string $needle, string|callable $message = null, string $propertyPath = null, string $encoding = 'utf8') Assert that string ends with a sequence of chars. + * @method LazyAssertion enumCase(string $enumClass, string|callable $message = null, ?string $propertyPath = null) Assert that the value is a valid backed enum case. + * @method LazyAssertion enumExists(string|callable $message = null, ?string $propertyPath = null) Assert that the enum exists. * @method LazyAssertion eq(mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that two values are equal (using ==). * @method LazyAssertion eqArraySubset(mixed $value2, string|callable $message = null, string $propertyPath = null) Assert that the array contains the subset. * @method LazyAssertion extensionLoaded(string|callable $message = null, string $propertyPath = null) Assert that extension is loaded. diff --git a/phpstan-tests.neon b/phpstan-tests.neon index e9ab0bcf..df76b213 100644 --- a/phpstan-tests.neon +++ b/phpstan-tests.neon @@ -4,7 +4,7 @@ parameters: - '#Call to an undefined method Assert\\AssertionChain::unknownAssertion\(\)#' - '#Call to an undefined static method Assert\\Assertion::nullOrAssertionDoesNotExist\(\)#' - '#Class Foo not found#' - - '#Parameter \#1 $value of static method Assert\\Assertion::isCountable\(\) expects array|Countable|ResourceBundle|SimpleXMLElement, string given#' + - '#Parameter \#1 \$value of static method Assert\\Assertion::isCountable\(\) expects array|Countable|ResourceBundle|SimpleXMLElement, string given#' - '#Parameter \#2 \$operator of static method Assert\\Assertion::version\(\) expects string, null given#' - '#Static method Assert\\Assertion::allTrue\(\) invoked with 0 parameters, 1-3 required#' - '#Static method Assert\\Assertion::nullOrMax\(\) invoked with 0 parameters, 2-4 required#' diff --git a/tests/Assert/Tests/AssertEnumTest.php b/tests/Assert/Tests/AssertEnumTest.php new file mode 100644 index 00000000..4dcadb0f --- /dev/null +++ b/tests/Assert/Tests/AssertEnumTest.php @@ -0,0 +1,68 @@ +assertTrue(Assertion::enumExists(TestUnitEnum::class)); + } + + public function testInvalidEnumExists() + { + $this->expectException('Assert\AssertionFailedException'); + $this->expectExceptionCode(\Assert\Assertion::INVALID_ENUM); + Assertion::enumExists('InvalidEnum'); + } + + public function testValidEnumCase() + { + $this->assertTrue(Assertion::enumCase('one', TestBackedEnum::class)); + } + + public function testEnumCaseInvalidValue() + { + $this->expectException('Assert\AssertionFailedException'); + $this->expectExceptionCode(\Assert\Assertion::INVALID_ENUM_CASE); + Assertion::enumCase('three', TestBackedEnum::class); + } + + public function testEnumCaseInvalidEnum() + { + $this->expectException('Assert\AssertionFailedException'); + $this->expectExceptionCode(\Assert\Assertion::INVALID_ENUM); + Assertion::enumCase('three', 'InvalidEnum'); + } + + public function testEnumCaseNonBackedEnum() + { + $this->expectException('Assert\AssertionFailedException'); + $this->expectExceptionCode(\Assert\Assertion::INTERFACE_NOT_IMPLEMENTED); + Assertion::enumCase('first', TestUnitEnum::class); + } +} diff --git a/tests/Assert/Tests/Fixtures/TestBackedEnum.php b/tests/Assert/Tests/Fixtures/TestBackedEnum.php new file mode 100644 index 00000000..7d7463f5 --- /dev/null +++ b/tests/Assert/Tests/Fixtures/TestBackedEnum.php @@ -0,0 +1,20 @@ +