From e36fd559daeecf1ad022a8fd0ba29d111216fc96 Mon Sep 17 00:00:00 2001 From: Laurent Clouet Date: Sun, 27 Sep 2020 20:56:29 +0200 Subject: [PATCH] [Validator] Add Ulid constraint and validator --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Component/Validator/Constraints/Ulid.php | 36 ++++++++ .../Validator/Constraints/UlidValidator.php | 69 +++++++++++++++ .../Tests/Constraints/UlidValidatorTest.php | 83 +++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/Ulid.php create mode 100644 src/Symfony/Component/Validator/Constraints/UlidValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index f980454e2216c..b77219de92f49 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -30,6 +30,7 @@ CHANGELOG */ ``` * added the `Isin` constraint and validator + * added the `ULID` constraint and validator 5.1.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/Ulid.php b/src/Symfony/Component/Validator/Constraints/Ulid.php new file mode 100644 index 0000000000000..cf411070302f1 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Ulid.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * @Annotation + * + * @author Laurent Clouet + */ +class Ulid extends Constraint +{ + const TOO_SHORT_ERROR = '7b44804e-37d5-4df4-9bdd-b738d4a45bb4'; + const TOO_LONG_ERROR = '9608249f-6da1-4d53-889e-9864b58c4d37'; + const INVALID_CHARACTERS_ERROR = 'e4155739-5135-4258-9c81-ae7b44b5311e'; + const TOO_LARGE_ERROR = 'df8cfb9a-ce6d-4a69-ae5a-eea7ab6f278b'; + + protected static $errorNames = [ + self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', + self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', + self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', + self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', + ]; + + public $message = 'This is not a valid ULID.'; +} diff --git a/src/Symfony/Component/Validator/Constraints/UlidValidator.php b/src/Symfony/Component/Validator/Constraints/UlidValidator.php new file mode 100644 index 0000000000000..45f85b852d237 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/UlidValidator.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * Validates whether the value is a valid ULID (Universally Unique Lexicographically Sortable Identifier). + * Cf https://github.com/ulid/spec for ULID specifications. + * + * @author Laurent Clouet + */ +class UlidValidator extends ConstraintValidator +{ + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof Ulid) { + throw new UnexpectedTypeException($constraint, Ulid::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + + if (26 !== \strlen($value)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(26 > \strlen($value) ? Ulid::TOO_SHORT_ERROR : Ulid::TOO_LONG_ERROR) + ->addViolation(); + } + + if (\strlen($value) !== strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Ulid::INVALID_CHARACTERS_ERROR) + ->addViolation(); + } + + // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ' + // Cf https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings + if ($value[0] > '7') { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Ulid::TOO_LARGE_ERROR) + ->addViolation(); + } + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php new file mode 100644 index 0000000000000..0f184c85c8a87 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use stdClass; +use Symfony\Component\Validator\Constraints\Ulid; +use Symfony\Component\Validator\Constraints\UlidValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +/** + * @author Laurent Clouet + */ +class UlidValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator() + { + return new UlidValidator(); + } + + public function testNullIsValid() + { + $this->validator->validate(null, new Ulid()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new Ulid()); + + $this->assertNoViolation(); + } + + public function testExpectsStringCompatibleType() + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new stdClass(), new Ulid()); + } + + public function testValidUlid() + { + $this->validator->validate('01ARZ3NDEKTSV4RRFFQ69G5FAV', new Ulid()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getInvalidUlids + */ + public function testInvalidUlid(string $ulid, string $code) + { + $constraint = new Ulid([ + 'message' => 'testMessage', + ]); + + $this->validator->validate($ulid, $constraint); + + $this->buildViolation('testMessage') + ->setParameter('{{ value }}', '"'.$ulid.'"') + ->setCode($code) + ->assertRaised(); + } + + public function getInvalidUlids() + { + return [ + ['01ARZ3NDEKTSV4RRFFQ69G5FA', Ulid::TOO_SHORT_ERROR], + ['01ARZ3NDEKTSV4RRFFQ69G5FAVA', Ulid::TOO_LONG_ERROR], + ['01ARZ3NDEKTSV4RRFFQ69G5FAO', Ulid::INVALID_CHARACTERS_ERROR], + ['Z1ARZ3NDEKTSV4RRFFQ69G5FAV', Ulid::TOO_LARGE_ERROR], + ]; + } +}