Skip to content

Trace paths instead of IDs #1532

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
140 changes: 140 additions & 0 deletions library/FailedResultIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

declare(strict_types=1);

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

namespace Respect\Validation;

use Countable;
use Iterator;
use RecursiveIterator;

use function array_filter;
use function array_key_exists;
use function array_map;
use function array_values;
use function count;
use function current;
use function key;
use function next;
use function reset;

/**
* @implements Iterator<int, Result>
* @implements RecursiveIterator<int, Result>
*/
final class FailedResultIterator implements Iterator, Countable, RecursiveIterator
{
/**
* @var array<int, Result>
*/
private array $children;

public function __construct(
private readonly Result $result,
) {
$this->children = $this->extractDeduplicatedChildren();
}

public function current(): Result|false
{
return current($this->children);
}

/**
* @return array<int, Result>
*/
public function getArrayCopy(): array
{
return $this->children;
}

public function next(): void
{
next($this->children);
}

public function key(): ?int

Check warning on line 61 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L61

Added line #L61 was not covered by tests
{
return key($this->children);

Check warning on line 63 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L63

Added line #L63 was not covered by tests
}

public function valid(): bool
{
return key($this->children) !== null;
}

public function rewind(): void
{
reset($this->children);
}

public function count(): int
{
return count($this->children);
}

public function hasChildren(): bool

Check warning on line 81 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L81

Added line #L81 was not covered by tests
{
return $this->result->children !== [];

Check warning on line 83 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L83

Added line #L83 was not covered by tests
}

/**
* @return null|RecursiveIterator<int, Result>
*/
public function getChildren(): ?RecursiveIterator

Check warning on line 89 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L89

Added line #L89 was not covered by tests
{
if (!$this->hasChildren()) {
return null;

Check warning on line 92 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L91-L92

Added lines #L91 - L92 were not covered by tests
}

return new self($this->result);

Check warning on line 95 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L95

Added line #L95 was not covered by tests
}

/**
* @return array<int, Result>
*/
private function extractDeduplicatedChildren(): array
{
/** @var array<string, Result> $deduplicatedResults */
$deduplicatedResults = [];
$duplicateCounters = [];
foreach ($this->result->children as $child) {
if ($child->path !== null) {
$deduplicatedResults[$child->path] = $child->hasPassed ? null : $child;
continue;
}

$id = $child->id;
if (isset($duplicateCounters[$id])) {
$id .= '.' . ++$duplicateCounters[$id];

Check warning on line 114 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L114

Added line #L114 was not covered by tests
} elseif (array_key_exists($id, $deduplicatedResults)) {
$deduplicatedResults[$id . '.1'] = $deduplicatedResults[$id]?->withId($id . '.1');
unset($deduplicatedResults[$id]);
$duplicateCounters[$id] = 2;
$id .= '.2';

Check warning on line 119 in library/FailedResultIterator.php

View check run for this annotation

Codecov / codecov/patch

library/FailedResultIterator.php#L116-L119

Added lines #L116 - L119 were not covered by tests
}

$deduplicatedResults[$id] = $child->hasPassed ? null : $child->withId($id);
}

return array_map(
function (Result $child): Result {
if ($this->result->path !== null && $child->path !== null && $child->path !== $this->result->path) {
return $child->withPath($this->result->path);
}

if ($this->result->path !== null && $child->path === null) {
return $child->withPath($this->result->path);
}

return $child;
},
array_values(array_filter($deduplicatedResults))
);
}
}
23 changes: 23 additions & 0 deletions library/Message/Placeholder/Path.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

namespace Respect\Validation\Message\Placeholder;

final class Path
{
public function __construct(
private readonly int|string $value
) {
}

public function getValue(): int|string
{
return $this->value;
}
}
45 changes: 6 additions & 39 deletions library/Message/StandardFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@
namespace Respect\Validation\Message;

use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\FailedResultIterator;
use Respect\Validation\Result;

use function array_filter;
use function array_key_exists;
use function array_map;
use function array_reduce;
use function array_values;
use function count;
use function current;
use function is_array;
Expand All @@ -42,8 +40,8 @@ public function main(Result $result, array $templates, Translator $translator):
{
$selectedTemplates = $this->selectTemplates($result, $templates);
if (!$this->isFinalTemplate($result, $selectedTemplates)) {
foreach ($this->extractDeduplicatedChildren($result) as $child) {
return $this->main($this->resultWithPath($result, $child), $selectedTemplates, $translator);
foreach (new FailedResultIterator($result) as $child) {
return $this->main($child, $selectedTemplates, $translator);
}
}

Expand Down Expand Up @@ -78,17 +76,14 @@ public function full(
}

if (!$isFinalTemplate) {
$results = array_map(
fn(Result $child) => $this->resultWithPath($result, $child),
$this->extractDeduplicatedChildren($result)
);
$results = new FailedResultIterator($result);
foreach ($results as $child) {
$rendered .= $this->full(
$child,
$selectedTemplates,
$translator,
$depth,
...array_filter($results, static fn (Result $sibling) => $sibling !== $child)
...array_filter($results->getArrayCopy(), static fn (Result $sibling) => $sibling !== $child)
);
$rendered .= PHP_EOL;
}
Expand All @@ -105,7 +100,7 @@ public function full(
public function array(Result $result, array $templates, Translator $translator): array
{
$selectedTemplates = $this->selectTemplates($result, $templates);
$deduplicatedChildren = $this->extractDeduplicatedChildren($result);
$deduplicatedChildren = new FailedResultIterator($result);
if (count($deduplicatedChildren) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) {
return [
$result->getDeepestPath() ?? $result->id => $this->renderer->render(
Expand Down Expand Up @@ -256,32 +251,4 @@ private function selectTemplates(Result $result, array $templates): array

return $templates;
}

/** @return array<Result> */
private function extractDeduplicatedChildren(Result $result): array
{
/** @var array<string, Result> $deduplicatedResults */
$deduplicatedResults = [];
$duplicateCounters = [];
foreach ($result->children as $child) {
$id = $child->getDeepestPath() ?? $child->id;
if (isset($duplicateCounters[$id])) {
$id .= '.' . ++$duplicateCounters[$id];
} elseif (array_key_exists($id, $deduplicatedResults)) {
$deduplicatedResults[$id . '.1'] = $deduplicatedResults[$id]?->withId($id . '.1');
unset($deduplicatedResults[$id]);
$duplicateCounters[$id] = 2;
$id .= '.2';
}

if ($child->path === null) {
$deduplicatedResults[$id] = $child->hasPassed ? null : $child->withId($id);
continue;
}

$deduplicatedResults[$id] = $child->hasPassed ? null : $child;
}

return array_values(array_filter($deduplicatedResults));
}
}
3 changes: 2 additions & 1 deletion library/Message/StandardRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use ReflectionClass;
use Respect\Stringifier\Stringifier;
use Respect\Validation\Message\Placeholder\Listed;
use Respect\Validation\Message\Placeholder\Path;
use Respect\Validation\Message\Placeholder\Quoted;
use Respect\Validation\Result;
use Respect\Validation\Rule;
Expand All @@ -36,7 +37,7 @@ public function __construct(
public function render(Result $result, Translator $translator, ?string $template = null): string
{
$parameters = $result->parameters;
$parameters['path'] = $result->path !== null ? Quoted::fromPath($result->path) : null;
$parameters['path'] = $result->path !== null ? new Path($result->path) : null;
$parameters['input'] = $result->input;

$builtName = $result->name ?? $parameters['path'] ?? $this->placeholder('input', $result->input, $translator);
Expand Down
2 changes: 2 additions & 0 deletions library/Message/StandardStringifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Respect\Stringifier\Stringifiers\StringableObjectStringifier;
use Respect\Stringifier\Stringifiers\ThrowableObjectStringifier;
use Respect\Validation\Message\Stringifier\ListedStringifier;
use Respect\Validation\Message\Stringifier\PathStringifier;
use Respect\Validation\Message\Stringifier\QuotedStringifier;

final class StandardStringifier implements Stringifier
Expand Down Expand Up @@ -88,6 +89,7 @@ private function createStringifier(Quoter $quoter): Stringifier
$stringifier->prependStringifier(new ThrowableObjectStringifier($jsonEncodableStringifier, $quoter));
$stringifier->prependStringifier(new DateTimeStringifier($quoter, DateTimeInterface::ATOM));
$stringifier->prependStringifier(new IteratorObjectStringifier($stringifier, $quoter));
$stringifier->prependStringifier(new PathStringifier($quoter));
$stringifier->prependStringifier(new QuotedStringifier($quoter));
$stringifier->prependStringifier(new ListedStringifier($stringifier));

Expand Down
31 changes: 31 additions & 0 deletions library/Message/Stringifier/PathStringifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

namespace Respect\Validation\Message\Stringifier;

use Respect\Stringifier\Quoter;
use Respect\Stringifier\Stringifier;
use Respect\Validation\Message\Placeholder\Path;

final class PathStringifier implements Stringifier
{
public function __construct(
private readonly Quoter $quoter
) {
}

public function stringify(mixed $raw, int $depth): ?string
{
if (!$raw instanceof Path) {
return null;
}

return $this->quoter->quote('.' . $raw->getValue(), $depth);
}
}
7 changes: 6 additions & 1 deletion library/Rules/Cnh.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use function is_scalar;
use function mb_strlen;
use function preg_match;
use function preg_replace;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
Expand All @@ -34,7 +35,11 @@ public function isValid(mixed $input): bool
$input = (string) preg_replace('{\D}', '', (string) $input);

// Validate length and invalid numbers
if (mb_strlen($input) != 11 || ((int) $input === 0)) {
if (mb_strlen($input) != 11) {
return false;
}

if (preg_match('/^(\d)\1{10}/', $input) > 0) {
return false;
}

Expand Down
4 changes: 2 additions & 2 deletions library/Transformers/Deprecated/KeyValueRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Respect\Validation\Transformers\Deprecated;

use Respect\Validation\Message\Placeholder\Quoted;
use Respect\Validation\Message\Placeholder\Path;
use Respect\Validation\Rules\AlwaysInvalid;
use Respect\Validation\Rules\Key;
use Respect\Validation\Rules\KeyExists;
Expand Down Expand Up @@ -56,7 +56,7 @@ static function ($input) use ($comparedKey, $ruleName, $baseKey) {
return new Templated(
new AlwaysInvalid(),
'{{baseKey}} must be valid to validate {{comparedKey}}',
['comparedKey' => Quoted::fromPath($comparedKey), 'baseKey' => Quoted::fromPath($baseKey)]
['comparedKey' => new Path($comparedKey), 'baseKey' => new Path($baseKey)],
);
}
}
Expand Down
6 changes: 1 addition & 5 deletions tests/feature/Issues/Issue1289Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@
[
0 => [
'__root__' => '`.0` must pass the rules',
'default' => [
'__root__' => '`.default` must pass one of the rules',
'stringType' => '`.default` must be a string',
'boolType' => '`.default` must be a boolean',
],
'default' => '`.default` must be a boolean',
'description' => '`.description` must be a string value',
],
],
Expand Down
6 changes: 1 addition & 5 deletions tests/feature/Issues/Issue1376Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@
'__root__' => '`stdClass { +$author="foo" }` must pass all the rules',
'title' => '`.title` must be present',
'description' => '`.description` must be present',
'author' => [
'__root__' => '`.author` must pass all the rules',
'intType' => '`.author` must be an integer',
'lengthBetween' => 'The length of `.author` must be between 1 and 2',
],
'author' => 'The length of `.author` must be between 1 and 2',
'user' => '`.user` must be present',
],
));
6 changes: 1 addition & 5 deletions tests/feature/Rules/AttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@
[
'__root__' => '`Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$birthdate="not a date" +$email="not an email" +$phone ... }` must pass the rules',
'name' => '`.name` must not be empty',
'birthdate' => [
'__root__' => '`.birthdate` must pass all the rules',
'date' => '`.birthdate` must be a valid date in the format "2005-12-30"',
'dateTimeDiffLessThanOrEqual' => 'For comparison with now, `.birthdate` must be a valid datetime',
],
'birthdate' => 'For comparison with now, `.birthdate` must be a valid datetime',
'email' => '`.email` must be a valid email address or must be null',
'phone' => '`.phone` must be a valid telephone number or must be null',
],
Expand Down
Loading