Skip to content

Commit

Permalink
Trace paths instead of IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
henriquemoody committed Dec 27, 2024
1 parent 4379f66 commit 882e18a
Show file tree
Hide file tree
Showing 44 changed files with 505 additions and 493 deletions.
5 changes: 5 additions & 0 deletions library/Message/Placeholder/Quoted.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public function __construct(
) {
}

public static function fromPath(int|string $path): self
{
return new self('.' . $path);
}

public function getValue(): string
{
return $this->value;
Expand Down
86 changes: 53 additions & 33 deletions library/Message/StandardFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@ public function __construct(
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*/
public function main(Result $result, array $templates, Translator $translator): string
{
$selectedTemplates = $this->selectTemplates($result, $templates);
if (!$this->isFinalTemplate($result, $selectedTemplates)) {
foreach ($this->extractDeduplicatedChildren($result) as $child) {
if ($result->path !== null && $child->path !== null) {
$child = $child->withPath(sprintf('%s.%s', $result->path, $child->path));
} elseif ($result->path !== null && $child->path === null) {
$child = $child->withPath($result->path);
}

return $this->main($child, $selectedTemplates, $translator);
}
}
Expand All @@ -50,7 +56,7 @@ public function main(Result $result, array $templates, Translator $translator):
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*/
public function full(
Result $result,
Expand Down Expand Up @@ -91,32 +97,36 @@ public function full(
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*
* @return array<string, mixed>
* @return array<string|int, mixed>
*/
public function array(Result $result, array $templates, Translator $translator): array
{
$selectedTemplates = $this->selectTemplates($result, $templates);
$deduplicatedChildren = $this->extractDeduplicatedChildren($result);
if (count($deduplicatedChildren) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) {
return [
$result->id => $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator),
$result->path ?? $result->id => $this->renderer->render(
$this->getTemplated($result, $selectedTemplates),
$translator
),
];
}

$messages = [];
foreach ($deduplicatedChildren as $child) {
$messages[$child->id] = $this->array(
$key = $child->path ?? $child->id;
$messages[$key] = $this->array(
$child,
$this->selectTemplates($child, $selectedTemplates),
$translator
);
if (count($messages[$child->id]) !== 1) {
if (count($messages[$key]) !== 1) {
continue;
}

$messages[$child->id] = current($messages[$child->id]);
$messages[$key] = current($messages[$key]);
}

if (count($messages) > 1) {
Expand Down Expand Up @@ -165,56 +175,66 @@ private function isAlwaysVisible(Result $result, Result ...$siblings): bool
);
}

/** @param array<string, mixed> $templates */
/** @param array<string|int, mixed> $templates */
private function getTemplated(Result $result, array $templates): Result
{
if ($result->hasCustomTemplate()) {
return $result;
}

if (!isset($templates[$result->id]) && isset($templates['__root__'])) {
return $result->withTemplate($templates['__root__']);
}
foreach ([$result->path, $result->name, $result->id, '__root__'] as $key) {
if (!isset($templates[$key])) {
continue;
}

if (!isset($templates[$result->id])) {
return $result;
}
if (is_string($templates[$key])) {
return $result->withTemplate($templates[$key]);
}

$template = $templates[$result->id];
if (is_string($template)) {
return $result->withTemplate($template);
throw new ComponentException(
sprintf('Template for "%s" must be a string, %s given', $key, stringify($templates[$key]))
);
}

throw new ComponentException(
sprintf('Template for "%s" must be a string, %s given', $result->id, stringify($template))
);
return $result;
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*/
private function isFinalTemplate(Result $result, array $templates): bool
{
if (isset($templates[$result->id]) && is_string($templates[$result->id])) {
return true;
$keys = [$result->path, $result->name, $result->id];
foreach ($keys as $key) {
if (isset($templates[$key]) && is_string($templates[$key])) {
return true;
}
}

if (count($templates) !== 1) {
return false;
}

return isset($templates['__root__']) || isset($templates[$result->id]);
foreach ($keys as $key) {
if (isset($templates[$key])) {
return true;
}
}

return isset($templates['__root__']);
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*
* @return array<string, mixed>
* @return array<string|int, mixed>
*/
private function selectTemplates(Result $message, array $templates): array
private function selectTemplates(Result $result, array $templates): array
{
if (isset($templates[$message->id]) && is_array($templates[$message->id])) {
return $templates[$message->id];
foreach ([$result->path, $result->name, $result->id] as $key) {
if (isset($templates[$key]) && is_array($templates[$key])) {
return $templates[$key];
}
}

return $templates;
Expand All @@ -227,16 +247,16 @@ private function extractDeduplicatedChildren(Result $result): array
$deduplicatedResults = [];
$duplicateCounters = [];
foreach ($result->children as $child) {
$id = $child->id;
$id = $child->path ?? $child->id;
if (isset($duplicateCounters[$id])) {
$id .= '.' . ++$duplicateCounters[$id];
} elseif (array_key_exists($id, $deduplicatedResults)) {
$deduplicatedResults[$id . '.1'] = $deduplicatedResults[$id]?->withId($id . '.1');
$deduplicatedResults[$id . '.1'] = $child->path ? $deduplicatedResults[$id]?->withPath($id . '.1') : $deduplicatedResults[$id]?->withId($id . '.1');
unset($deduplicatedResults[$id]);
$duplicateCounters[$id] = 2;
$id .= '.2';
}
$deduplicatedResults[$id] = $child->isValid ? null : $child->withId($id);
$deduplicatedResults[$id] = $child->isValid ? null : ($child->path ? $child->withPath((string) $id) : $child->withId((string) $id));
}

return array_values(array_filter($deduplicatedResults));
Expand Down
2 changes: 1 addition & 1 deletion library/Message/StandardRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function __construct(
public function render(Result $result, Translator $translator, ?string $template = null): string
{
$parameters = $result->parameters;
$parameters['name'] ??= $result->name ?? $this->placeholder('input', $result->input, $translator);
$parameters['name'] ??= $result->name ?? ($result->path !== null ? Quoted::fromPath($result->path) : null) ?? $this->placeholder('input', $result->input, $translator);
$parameters['input'] = $result->input;

$rendered = (string) preg_replace_callback(
Expand Down
16 changes: 6 additions & 10 deletions library/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function __construct(
public readonly ?string $name = null,
?string $id = null,
public readonly ?Result $adjacent = null,
public readonly bool $unchangeableId = false,
public readonly string|int|null $path = null,
Result ...$children,
) {
$this->id = $id ?? lcfirst(substr((string) strrchr($rule::class, '\\'), 1));
Expand Down Expand Up @@ -102,10 +102,6 @@ public function withExtraParameters(array $parameters): self

public function withId(string $id): self
{
if ($this->unchangeableId) {
return $this;
}

return $this->clone(id: $id);
}

Expand All @@ -114,14 +110,14 @@ public function withIdFrom(Rule $rule): self
return $this->clone(id: lcfirst(substr((string) strrchr($rule::class, '\\'), 1)));
}

public function withUnchangeableId(string $id): self
public function withPath(string|int $path): self
{
return $this->clone(id: $id, unchangeableId: true);
return $this->clone(adjacent: $this->adjacent?->withPath($path), path: $path);
}

public function withPrefix(string $prefix): self
{
if ($this->id === $this->name || $this->unchangeableId) {
if ($this->id === $this->name || $this->path !== null) {
return $this;
}

Expand Down Expand Up @@ -223,7 +219,7 @@ private function clone(
?string $name = null,
?string $id = null,
?Result $adjacent = null,
?bool $unchangeableId = null,
string|int|null $path = null,
?array $children = null
): self {
return new self(
Expand All @@ -236,7 +232,7 @@ private function clone(
$name ?? $this->name,
$id ?? $this->id,
$adjacent ?? $this->adjacent,
$unchangeableId ?? $this->unchangeableId,
$path ?? $this->path,
...($children ?? $this->children)
);
}
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/Each.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected function evaluateNonEmptyArray(array $input): Result
{
$children = [];
foreach ($input as $key => $value) {
$children[] = $this->rule->evaluate($value)->withUnchangeableId((string) $key);
$children[] = $this->rule->evaluate($value)->withPath($key);
}
$isValid = array_reduce($children, static fn ($carry, $childResult) => $carry && $childResult->isValid, true);

Expand Down
17 changes: 2 additions & 15 deletions library/Rules/Key.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use Respect\Validation\Result;
use Respect\Validation\Rule;
use Respect\Validation\Rules\Core\KeyRelated;
use Respect\Validation\Rules\Core\Nameable;
use Respect\Validation\Rules\Core\Wrapper;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
Expand All @@ -35,21 +34,9 @@ public function evaluate(mixed $input): Result
{
$keyExistsResult = (new KeyExists($this->key))->evaluate($input);
if (!$keyExistsResult->isValid) {
return $keyExistsResult->withName($this->getName());
return $keyExistsResult->withNameFrom($this->rule);
}

return $this->rule
->evaluate($input[$this->key])
->withName($this->getName())
->withUnchangeableId((string) $this->key);
}

private function getName(): string
{
if ($this->rule instanceof Nameable) {
return $this->rule->getName() ?? ((string) $this->key);
}

return (string) $this->key;
return $this->rule->evaluate($input[$this->key])->withPath($this->key);
}
}
2 changes: 1 addition & 1 deletion library/Rules/KeyExists.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function getKey(): int|string

public function evaluate(mixed $input): Result
{
return new Result($this->hasKey($input), $input, $this, name: (string) $this->key, id: (string) $this->key);
return new Result($this->hasKey($input), $input, $this, path: $this->key);
}

private function hasKey(mixed $input): bool
Expand Down
15 changes: 2 additions & 13 deletions library/Rules/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use ReflectionObject;
use Respect\Validation\Result;
use Respect\Validation\Rule;
use Respect\Validation\Rules\Core\Nameable;
use Respect\Validation\Rules\Core\Wrapper;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
Expand All @@ -30,22 +29,12 @@ public function evaluate(mixed $input): Result
{
$propertyExistsResult = (new PropertyExists($this->propertyName))->evaluate($input);
if (!$propertyExistsResult->isValid) {
return $propertyExistsResult->withName($this->getName());
return $propertyExistsResult->withNameFrom($this->rule);
}

return $this->rule
->evaluate($this->extractPropertyValue($input, $this->propertyName))
->withName($this->getName())
->withUnchangeableId($this->propertyName);
}

private function getName(): string
{
if ($this->rule instanceof Nameable) {
return $this->rule->getName() ?? $this->propertyName;
}

return $this->propertyName;
->withPath($this->propertyName);
}

private function extractPropertyValue(object $input, string $property): mixed
Expand Down
3 changes: 1 addition & 2 deletions library/Rules/PropertyExists.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ public function evaluate(mixed $input): Result
$this->hasProperty($input),
$input,
$this,
name: $this->propertyName,
id: $this->propertyName
path: $this->propertyName,
);
}

Expand Down
5 changes: 3 additions & 2 deletions library/Transformers/DeprecatedKeyValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace Respect\Validation\Transformers;

use Respect\Validation\Message\Placeholder\Quoted;
use Respect\Validation\Rules\AlwaysInvalid;
use Respect\Validation\Rules\Key;
use Respect\Validation\Rules\KeyExists;
Expand Down Expand Up @@ -52,8 +53,8 @@ static function ($input) use ($comparedKey, $ruleName, $baseKey) {
} catch (Throwable) {
return new Templated(
new AlwaysInvalid(),
'{{baseKey|raw}} must be valid to validate {{comparedKey|raw}}',
['comparedKey' => $comparedKey, 'baseKey' => $baseKey]
'{{baseKey}} must be valid to validate {{comparedKey}}',
['comparedKey' => Quoted::fromPath($comparedKey), 'baseKey' => Quoted::fromPath($baseKey)]
);
}
}
Expand Down
20 changes: 10 additions & 10 deletions tests/feature/AssertWithKeysTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@
]),
<<<'FULL_MESSAGE'
- All the required rules must pass for the given data
- All the required rules must pass for mysql
- host must be a string
- user must be present
- password must be present
- schema must be a string
- All the required rules must pass for postgresql
- host must be present
- user must be a string
- password must be a string
- schema must be present
- All the required rules must pass for `.mysql`
- `.host` must be a string
- `.user` must be present
- `.password` must be present
- `.schema` must be a string
- All the required rules must pass for `.postgresql`
- `.host` must be present
- `.user` must be a string
- `.password` must be a string
- `.schema` must be present
FULL_MESSAGE,
));
Loading

0 comments on commit 882e18a

Please sign in to comment.