Skip to content

[Validator] Add Ulid constraint and validator #38322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ CHANGELOG
*/
```
* added the `Isin` constraint and validator
* added the `ULID` constraint and validator

5.1.0
-----
Expand Down
36 changes: 36 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Ulid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <laurent35240@gmail.com>
*/
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.';
}
69 changes: 69 additions & 0 deletions src/Symfony/Component/Validator/Constraints/UlidValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <laurent35240@gmail.com>
*/
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')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

26 !== strspn([...]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more sense to keep \strlen($value) otherwise a too short ULID but with only valid characters will have violations about being too short AND having invalid characters.
The violation about invalid characters would not be true in this case.

Let me know what you think about it

$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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <laurent35240@gmail.com>
*/
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],
];
}
}