Skip to content

Commit

Permalink
Update the validation engine of the "Domain" rule
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
henriquemoody committed Mar 6, 2024
1 parent 2610a38 commit 3a6a71a
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 155 deletions.
145 changes: 24 additions & 121 deletions library/Rules/Domain.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,27 @@

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)
{
Expand All @@ -44,102 +38,27 @@ public function __construct(bool $tldCheck = true)
$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
Expand All @@ -148,39 +67,23 @@ private function createTldRule(bool $realTldCheck): Validatable
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;
}
}
}
5 changes: 1 addition & 4 deletions tests/integration/rules/domain.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
91 changes: 61 additions & 30 deletions tests/unit/Rules/DomainTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,79 @@
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 [
['111111111111domain.local'],
['111111111111.domain.local'],
];
}

/** @return array<array{string}> */
public static function providerForDomainWithRealTopLevelDomain(): 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'],
['example.com'],
['xn--bcher-kva.ch'],
['mail.xn--bcher-kva.ch'],
['example-hyphen.com'],
['example--valid.com'],
['std--a.com'],
['r--w.com'],
];
}

/** @return iterable<array{Domain, mixed}> */
public static function providerForInvalidInput(): iterable
/** @return array<array{string}> */
public static function providerForInvalidDomains(): 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'],
[''],
['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'],
];
}
}

0 comments on commit 3a6a71a

Please sign in to comment.