Update the validation engine of the "Domain" rule

I also decided to make the messages way more straightforward than
before. Instead of showing why the input is not a valid domain, we're
now simply saying that the input is not a proper domain.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-03-07 00:42:19 +01:00
parent 2610a380dc
commit 3a6a71a1f8
No known key found for this signature in database
GPG key ID: 221E9281655813A6
3 changed files with 86 additions and 155 deletions

View file

@ -9,33 +9,27 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Standard;
use Respect\Validation\Validatable;
use function array_filter;
use function array_merge;
use function array_pop;
use function count;
use function explode;
use function iterator_to_array;
use function mb_substr_count;
#[ExceptionClass(NestedValidationException::class)]
#[Template(
'{{name}} must be a valid domain',
'{{name}} must not be a valid domain',
)]
final class Domain extends AbstractRule
final class Domain extends Standard
{
private readonly Consecutive $genericRule;
private readonly Validatable $genericRule;
private readonly Validatable $tldRule;
private readonly AllOf $partsRule;
private readonly Validatable $partsRule;
public function __construct(bool $tldCheck = true)
{
@ -44,102 +38,27 @@ final class Domain extends AbstractRule
$this->partsRule = $this->createPartsRule();
}
public function assert(mixed $input): void
{
$exceptions = [];
$this->collectAssertException($exceptions, $this->genericRule, $input);
$this->throwExceptions($exceptions, $input);
$parts = explode('.', (string) $input);
if (count($parts) >= 2) {
$this->collectAssertException($exceptions, $this->tldRule, array_pop($parts));
}
foreach ($parts as $part) {
$this->collectAssertException($exceptions, $this->partsRule, $part);
}
$this->throwExceptions($exceptions, $input);
}
public function evaluate(mixed $input): Result
{
$genericResult = $this->genericRule->evaluate($input);
if (!$genericResult->isValid) {
return (new Result(false, $input, $this))->withChildren($genericResult);
return Result::failed($input, $this);
}
$children = [];
$valid = true;
$parts = explode('.', (string) $input);
if (count($parts) >= 2) {
$tld = array_pop($parts);
$childResult = $this->tldRule->evaluate($tld);
$valid = $childResult->isValid;
$children[] = $childResult;
}
foreach ($parts as $part) {
$partsResult = $this->partsRule->evaluate($part);
$valid = $valid && $partsResult->isValid;
$children = array_merge($children, $partsResult->children);
}
return (new Result($valid, $input, $this))
->withChildren(...array_filter($children, static fn (Result $child) => !$child->isValid));
}
public function validate(mixed $input): bool
{
try {
$this->assert($input);
} catch (ValidationException $exception) {
return false;
}
return true;
}
public function check(mixed $input): void
{
try {
$this->assert($input);
} catch (NestedValidationException $exception) {
/** @var ValidationException $childException */
foreach ($exception as $childException) {
throw $childException;
$childResult = $this->tldRule->evaluate(array_pop($parts));
if (!$childResult->isValid) {
return Result::failed($input, $this);
}
throw $exception;
}
}
/**
* @param ValidationException[] $exceptions
*/
private function collectAssertException(array &$exceptions, Validatable $validator, mixed $input): void
{
try {
$validator->assert($input);
} catch (NestedValidationException $nestedValidationException) {
$exceptions = array_merge(
$exceptions,
iterator_to_array($nestedValidationException)
);
} catch (ValidationException $validationException) {
$exceptions[] = $validationException;
}
return new Result($this->partsRule->evaluate($parts)->isValid, $input, $this);
}
private function createGenericRule(): Consecutive
{
return new Consecutive(
new StringType(),
new NoWhitespace(),
new Contains('.'),
new Length(3)
);
return new Consecutive(new StringType(), new NoWhitespace(), new Contains('.'), new Length(3));
}
private function createTldRule(bool $realTldCheck): Validatable
@ -148,39 +67,23 @@ final class Domain extends AbstractRule
return new Tld();
}
return new Consecutive(
new Not(new StartsWith('-')),
new NoWhitespace(),
new Length(2)
);
return new Consecutive(new Not(new StartsWith('-')), new Length(2));
}
private function createPartsRule(): AllOf
private function createPartsRule(): Validatable
{
return new AllOf(
new Alnum('-'),
new Not(new StartsWith('-')),
new AnyOf(
new Not(new Contains('--')),
new Callback(static function ($str) {
return mb_substr_count($str, '--') == 1;
})
),
new Not(new EndsWith('-'))
return new Each(
new Consecutive(
new Alnum('-'),
new Not(new StartsWith('-')),
new AnyOf(
new Not(new Contains('--')),
new Callback(static function ($str) {
return mb_substr_count($str, '--') == 1;
})
),
new Not(new EndsWith('-'))
)
);
}
/**
* @param ValidationException[] $exceptions
*/
private function throwExceptions(array $exceptions, mixed $input): void
{
if (count($exceptions)) {
/** @var NestedValidationException $domainException */
$domainException = $this->reportError($input);
$domainException->addChildren($exceptions);
throw $domainException;
}
}
}

View file

@ -13,10 +13,7 @@ exceptionFullMessage(static fn() => v::domain()->assert('p-éz-.kk'));
exceptionFullMessage(static fn() => v::not(v::domain())->assert('github.com'));
?>
--EXPECT--
"batman" must contain the value "."
"batman" must be a valid domain
"r--w.com" must not be a valid domain
- "p-éz-.kk" must be a valid domain
- "kk" must be a valid top-level domain name
- "p-éz-" must contain only letters (a-z), digits (0-9) and "-"
- "p-éz-" must not end with "-"
- "github.com" must not be a valid domain

View file

@ -10,48 +10,79 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use Respect\Validation\Test\RuleTestCase;
use Respect\Validation\Test\Stubs\ToStringStub;
use stdClass;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\TestCase;
#[Group('rule')]
#[CoversClass(Domain::class)]
final class DomainTest extends RuleTestCase
final class DomainTest extends TestCase
{
/** @return iterable<array{Domain, mixed}> */
public static function providerForValidInput(): iterable
#[Test]
#[DataProvider('providerForDomainWithoutRealTopLevelDomain')]
public function itShouldValidateDomainsWithoutRealTopLevelDomain(string $input): void
{
self::assertValidInput(new Domain(false), $input);
}
#[Test]
#[DataProvider('providerForDomainWithRealTopLevelDomain')]
public function itShouldValidateDomainsWithRealTopLevelDomain(string $input): void
{
self::assertValidInput(new Domain(), $input);
}
#[Test]
#[DataProvider('providerForNonStringValues')]
public function itShouldInvalidWhenInputIsNotString(mixed $input): void
{
self::assertInvalidInput(new Domain(), $input);
}
#[Test]
#[DataProvider('providerForInvalidDomains')]
public function itShouldInvalidInvalidDomains(mixed $input): void
{
self::assertInvalidInput(new Domain(), $input);
}
/** @return array<array{string}> */
public static function providerForDomainWithoutRealTopLevelDomain(): array
{
return [
[new Domain(false), '111111111111domain.local'],
[new Domain(false), '111111111111.domain.local'],
[new Domain(), 'example.com'],
[new Domain(), 'xn--bcher-kva.ch'],
[new Domain(), 'mail.xn--bcher-kva.ch'],
[new Domain(), 'example-hyphen.com'],
[new Domain(), 'example--valid.com'],
[new Domain(), 'std--a.com'],
[new Domain(), 'r--w.com'],
['111111111111domain.local'],
['111111111111.domain.local'],
];
}
/** @return iterable<array{Domain, mixed}> */
public static function providerForInvalidInput(): iterable
/** @return array<array{string}> */
public static function providerForDomainWithRealTopLevelDomain(): array
{
return [
[new Domain(), null],
[new Domain(), new stdClass()],
[new Domain(), []],
[new Domain(), new ToStringStub('google.com')],
[new Domain(), ''],
[new Domain(), 'no dots'],
[new Domain(), '2222222domain.local'],
[new Domain(), '-example-invalid.com'],
[new Domain(), 'example.invalid.-com'],
[new Domain(), 'xn--bcher--kva.ch'],
[new Domain(), 'example.invalid-.com'],
[new Domain(), '1.2.3.256'],
[new Domain(), '1.2.3.4'],
['example.com'],
['xn--bcher-kva.ch'],
['mail.xn--bcher-kva.ch'],
['example-hyphen.com'],
['example--valid.com'],
['std--a.com'],
['r--w.com'],
];
}
/** @return array<array{string}> */
public static function providerForInvalidDomains(): array
{
return [
[''],
['no dots'],
['2222222domain.local'],
['-example-invalid.com'],
['example.invalid.-com'],
['xn--bcher--kva.ch'],
['example.invalid-.com'],
['1.2.3.256'],
['1.2.3.4'],
];
}
}