From cad83ce8f894aa57673178c8e5f85c182ebd92b9 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Tue, 17 Dec 2024 01:30:52 +0100 Subject: [PATCH] WIP: Improve messages in an array format - Name: That's what is in the placeholder, and could be used for path and id as well - Id: Tha could be the name of the rule, just a means to identify it and be able to build templates for it on that level. - Path: That's the real path of the rule, which will be useful when dealing with arrays or objects. --- library/Message/StandardFormatter.php | 118 ++++++++++++------ library/Result.php | 16 +-- library/Rules/Each.php | 2 +- library/Rules/Key.php | 2 +- library/Rules/KeyOptional.php | 2 +- library/Rules/Property.php | 2 +- library/Rules/PropertyOptional.php | 2 +- library/Validator.php | 6 +- tests/feature/Issues/Issue1289Test.php | 1 + tests/feature/Issues/Issue1334Test.php | 22 +++- tests/feature/Issues/Issue1348Test.php | 11 +- tests/feature/Issues/Issue1469Test.php | 24 ++-- tests/feature/Issues/Issue425Test.php | 5 +- tests/feature/Issues/Issue446Test.php | 5 +- tests/feature/Issues/Issue796Test.php | 10 +- tests/feature/Issues/Issue799Test.php | 5 +- tests/feature/Rules/AllOfTest.php | 5 +- tests/feature/Rules/AttributesTest.php | 15 ++- tests/feature/Rules/EachTest.php | 31 +++-- tests/feature/Rules/KeySetTest.php | 25 +++- tests/feature/Rules/PropertyTest.php | 17 +++ tests/library/Builders/ResultBuilder.php | 4 +- .../StandardFormatter/ArrayProvider.php | 5 +- 23 files changed, 236 insertions(+), 99 deletions(-) diff --git a/library/Message/StandardFormatter.php b/library/Message/StandardFormatter.php index d3d9a6b7d..424b2435c 100644 --- a/library/Message/StandardFormatter.php +++ b/library/Message/StandardFormatter.php @@ -14,12 +14,14 @@ use function array_filter; use function array_key_exists; +use function array_merge; use function array_reduce; use function array_values; use function count; use function current; use function is_array; use function is_string; +use function key; use function Respect\Stringifier\stringify; use function rtrim; use function sprintf; @@ -35,10 +37,14 @@ public function __construct( } /** - * @param array $templates + * @param array $templates */ public function main(Result $result, array $templates, Translator $translator): string { + if ($result->isValid) { + return ''; + } + $selectedTemplates = $this->selectTemplates($result, $templates); if (!$this->isFinalTemplate($result, $selectedTemplates)) { foreach ($this->extractDeduplicatedChildren($result) as $child) { @@ -50,7 +56,7 @@ public function main(Result $result, array $templates, Translator $translator): } /** - * @param array $templates + * @param array $templates */ public function full( Result $result, @@ -59,6 +65,10 @@ public function full( int $depth = 0, Result ...$siblings ): string { + if ($result->isValid) { + return ''; + } + $selectedTemplates = $this->selectTemplates($result, $templates); $isFinalTemplate = $this->isFinalTemplate($result, $selectedTemplates); @@ -91,43 +101,58 @@ public function full( } /** - * @param array $templates + * @param array $templates * - * @return array + * @return array */ public function array(Result $result, array $templates, Translator $translator): array { + if ($result->isValid) { + return []; + } + + $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), - ]; + + $key = $this->isFinalTemplate($result, $selectedTemplates) ? $result->path ?? $result->id : '__root__'; + $messages = [$key => $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator)]; + + if ($key !== '__root__') { + return $messages; } - $messages = []; + $children = []; + foreach ($deduplicatedChildren as $child) { - $messages[$child->id] = $this->array( + if ($child->isValid) { + continue; + } + $childKey = $child->path ?? $child->id; + + $children[$childKey] = $this->array( $child, $this->selectTemplates($child, $selectedTemplates), $translator ); - if (count($messages[$child->id]) !== 1) { + + if (count($children[$childKey]) !== 1) { continue; } - $messages[$child->id] = current($messages[$child->id]); - } + $grantChildKey = key($children[$childKey]); + if ($grantChildKey != $childKey) { + continue; + } - if (count($messages) > 1) { - $self = [ - '__root__' => $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator), - ]; + $children[$childKey] = current($children[$childKey]); + } - return $self + $messages; + if (count($children) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) { + return [$result->path ?? $result->id => $messages['__root__']]; } - return $messages; + return $messages + $children; } private function isAlwaysVisible(Result $result, Result ...$siblings): bool @@ -165,56 +190,69 @@ private function isAlwaysVisible(Result $result, Result ...$siblings): bool ); } - /** @param array $templates */ + /** @param array $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__']); + $keys = [$result->name, $result->path, $result->id, '__root__']; + foreach ($keys as $key) { + if (isset($templates[$key]) && is_string($templates[$key])) { + return $result->withTemplate($templates[$key]); + } } - if (!isset($templates[$result->id])) { + if (!isset($templates[$result->id]) && !isset($templates[$result->path]) && !isset($templates[$result->name])) { return $result; } - $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', $result->id, stringify($template)) + sprintf( + 'Template for "%s" must be a string, %s given', + $result->path ?? $result->name ?? $result->id, + stringify($templates) + ) ); } /** - * @param array $templates + * @param array $templates */ private function isFinalTemplate(Result $result, array $templates): bool { - if (isset($templates[$result->id]) && is_string($templates[$result->id])) { - return true; + $keys = [$result->name, $result->path, $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 $templates + * @param array $templates * - * @return array + * @return array */ - 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->name, $result->path, $result->id] as $key) { + if (isset($templates[$key]) && is_array($templates[$key])) { + return $templates[$key]; + } } return $templates; @@ -227,7 +265,7 @@ 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)) { @@ -236,7 +274,7 @@ private function extractDeduplicatedChildren(Result $result): array $duplicateCounters[$id] = 2; $id .= '.2'; } - $deduplicatedResults[$id] = $child->isValid ? null : $child->withId($id); + $deduplicatedResults[$id] = $child->isValid ? null : $child->withId((string) $id); } return array_values(array_filter($deduplicatedResults)); diff --git a/library/Result.php b/library/Result.php index f2905f815..8467a13f2 100644 --- a/library/Result.php +++ b/library/Result.php @@ -40,7 +40,7 @@ public function __construct( ?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->name = $rule->getName() ?? $name; @@ -99,21 +99,17 @@ public function withTemplate(string $template): self public function withId(string $id): self { - if ($this->unchangeableId) { - return $this; - } - return $this->clone(id: $id); } - public function withUnchangeableId(string $id): self + public function withPath(string|int $path): self { - return $this->clone(id: $id, unchangeableId: true); + return $this->clone(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; } @@ -200,7 +196,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( @@ -213,7 +209,7 @@ private function clone( $name ?? $this->name, $id ?? $this->id, $adjacent ?? $this->adjacent, - $unchangeableId ?? $this->unchangeableId, + $path ?? $this->path, ...($children ?? $this->children) ); } diff --git a/library/Rules/Each.php b/library/Rules/Each.php index 66f4b8108..acc2bb372 100644 --- a/library/Rules/Each.php +++ b/library/Rules/Each.php @@ -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); if ($isValid) { diff --git a/library/Rules/Key.php b/library/Rules/Key.php index 81b48e267..e30b85e4f 100644 --- a/library/Rules/Key.php +++ b/library/Rules/Key.php @@ -41,7 +41,7 @@ public function evaluate(mixed $input): Result return $this->rule ->evaluate($input[$this->key]) - ->withUnchangeableId((string) $this->key) + ->withPath($this->key) ->withNameIfMissing($this->rule->getName() ?? (string) $this->key); } } diff --git a/library/Rules/KeyOptional.php b/library/Rules/KeyOptional.php index 36d030749..4284aba0b 100644 --- a/library/Rules/KeyOptional.php +++ b/library/Rules/KeyOptional.php @@ -41,7 +41,7 @@ public function evaluate(mixed $input): Result return $this->rule ->evaluate($input[$this->key]) - ->withUnchangeableId((string) $this->key) + ->withPath($this->key) ->withNameIfMissing($this->rule->getName() ?? (string) $this->key); } } diff --git a/library/Rules/Property.php b/library/Rules/Property.php index ea057540c..d1e83e60a 100644 --- a/library/Rules/Property.php +++ b/library/Rules/Property.php @@ -38,7 +38,7 @@ public function evaluate(mixed $input): Result return $this->rule ->evaluate($this->extractPropertyValue($input, $this->propertyName)) - ->withUnchangeableId($this->propertyName) + ->withPath($this->propertyName) ->withNameIfMissing($this->rule->getName() ?? $this->propertyName); } } diff --git a/library/Rules/PropertyOptional.php b/library/Rules/PropertyOptional.php index 88cd90eda..8e867fbfb 100644 --- a/library/Rules/PropertyOptional.php +++ b/library/Rules/PropertyOptional.php @@ -38,7 +38,7 @@ public function evaluate(mixed $input): Result return $this->rule ->evaluate($this->extractPropertyValue($input, $this->propertyName)) - ->withUnchangeableId($this->propertyName) + ->withPath($this->propertyName) ->withNameIfMissing($this->rule->getName() ?? $this->propertyName); } } diff --git a/library/Validator.php b/library/Validator.php index 06ba7146c..23ebee95c 100644 --- a/library/Validator.php +++ b/library/Validator.php @@ -29,7 +29,7 @@ final class Validator implements Rule /** @var array */ private array $rules = []; - /** @var array */ + /** @var array */ private array $templates = []; private ?string $name = null; @@ -65,7 +65,7 @@ public function isValid(mixed $input): bool return $this->evaluate($input)->isValid; } - /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ + /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ public function assert(mixed $input, array|string|Throwable|callable|null $template = null): void { $result = $this->evaluate($input); @@ -99,7 +99,7 @@ public function assert(mixed $input, array|string|Throwable|callable|null $templ throw $template($exception); } - /** @param array $templates */ + /** @param array $templates */ public function setTemplates(array $templates): self { $this->templates = $templates; diff --git a/tests/feature/Issues/Issue1289Test.php b/tests/feature/Issues/Issue1289Test.php index aa0babbdc..9edf422d7 100644 --- a/tests/feature/Issues/Issue1289Test.php +++ b/tests/feature/Issues/Issue1289Test.php @@ -54,6 +54,7 @@ - description must be a string value FULL_MESSAGE, [ + '__root__' => 'Each item in `[["default": 2, "description": [], "children": ["nope"]]]` must be valid', 0 => [ '__root__' => 'These rules must pass for `["default": 2, "description": [], "children": ["nope"]]`', 'default' => [ diff --git a/tests/feature/Issues/Issue1334Test.php b/tests/feature/Issues/Issue1334Test.php index d61f2933a..411a55c1b 100644 --- a/tests/feature/Issues/Issue1334Test.php +++ b/tests/feature/Issues/Issue1334Test.php @@ -35,15 +35,31 @@ function (): void { - street must be a string FULL_MESSAGE, [ + '__root__' => 'These rules must pass for `[["region": "Oregon", "country": "USA", "other": 123], ["street": "", "region": "Oregon", "country": "USA"], ["s ... ]`', 'each' => [ '__root__' => 'Each item in `[["region": "Oregon", "country": "USA", "other": 123], ["street": "", "region": "Oregon", "country": "USA"], ["s ... ]` must be valid', 0 => [ '__root__' => 'These rules must pass for `["region": "Oregon", "country": "USA", "other": 123]`', 'street' => 'street must be present', - 'other' => 'other must be a string or must be null', + 'other' => [ + '__root__' => 'These rules must pass for other', + 'nullOrStringType' => 'other must be a string or must be null', + ], + ], + 1 => [ + '__root__' => 'These rules must pass for `["street": "", "region": "Oregon", "country": "USA"]`', + 'street' => [ + '__root__' => 'These rules must pass for street', + 'notEmpty' => 'street must not be empty', + ], + ], + 2 => [ + '__root__' => 'These rules must pass for `["street": 123, "region": "Oregon", "country": "USA"]`', + 'street' => [ + '__root__' => 'These rules must pass for street', + 'stringType' => 'street must be a string', + ], ], - 1 => 'street must not be empty', - 2 => 'street must be a string', ], ] )); diff --git a/tests/feature/Issues/Issue1348Test.php b/tests/feature/Issues/Issue1348Test.php index ced0cef7a..4f9797c0f 100644 --- a/tests/feature/Issues/Issue1348Test.php +++ b/tests/feature/Issues/Issue1348Test.php @@ -47,6 +47,7 @@ - model must be in `["F150", "Bronco"]` FULL_MESSAGE, [ + '__root__' => 'These rules must pass for `[["manufacturer": "Honda", "model": "Accord"], ["manufacturer": "Toyota", "model": "Rav4"], ["manufacturer": "Fo ... ]`', 'each' => [ '__root__' => 'Each item in `[["manufacturer": "Honda", "model": "Accord"], ["manufacturer": "Toyota", "model": "Rav4"], ["manufacturer": "Fo ... ]` must be valid', 2 => [ @@ -61,11 +62,17 @@ 'manufacturer' => 'manufacturer must be equal to "Toyota"', 'model' => 'model must be in `["Rav4", "Camry"]`', ], - 'allOf.3' => 'model must be in `["F150", "Bronco"]`', + 'allOf.3' => [ + '__root__' => 'These rules must pass for `["manufacturer": "Ford", "model": "not real"]`', + 'model' => 'model must be in `["F150", "Bronco"]`', + ], ], 3 => [ '__root__' => 'Only one of these rules must pass for `["manufacturer": "Honda", "model": "not valid"]`', - 'allOf.1' => 'model must be in `["Accord", "Fit"]`', + 'allOf.1' => [ + '__root__' => 'These rules must pass for `["manufacturer": "Honda", "model": "not valid"]`', + 'model' => 'model must be in `["Accord", "Fit"]`', + ], 'allOf.2' => [ '__root__' => 'All the required rules must pass for `["manufacturer": "Honda", "model": "not valid"]`', 'manufacturer' => 'manufacturer must be equal to "Toyota"', diff --git a/tests/feature/Issues/Issue1469Test.php b/tests/feature/Issues/Issue1469Test.php index b3756a809..b4b9e00b2 100644 --- a/tests/feature/Issues/Issue1469Test.php +++ b/tests/feature/Issues/Issue1469Test.php @@ -39,14 +39,24 @@ function (): void { - product_title2 must not be present FULL_MESSAGE, [ + '__root__' => 'These rules must pass for `["order_items": [["product_title": test(?string $description = null, ?Closure $closure = null): Pest\Support\Hig ... ]`', 'keySet' => [ - '__root__' => 'Each item in order_items must be valid', - 0 => 'quantity must be an integer value', - 1 => [ - '__root__' => 'order_items contains both missing and extra keys', - 'product_title' => 'product_title must be present', - 'quantity' => 'quantity must be present', - 'product_title2' => 'product_title2 must not be present', + '__root__' => '`["order_items": [["product_title": test(?string $description = null, ?Closure $closure = null): Pest\Support\Hig ... ]` validation failed', + 'each' => [ + '__root__' => 'Each item in order_items must be valid', + 0 => [ + '__root__' => 'order_items validation failed', + 'quantity' => [ + '__root__' => 'These rules must pass for quantity', + 'intVal' => 'quantity must be an integer value', + ], + ], + 1 => [ + '__root__' => 'order_items contains both missing and extra keys', + 'product_title' => 'product_title must be present', + 'quantity' => 'quantity must be present', + 'product_title2' => 'product_title2 must not be present', + ], ], ], ] diff --git a/tests/feature/Issues/Issue425Test.php b/tests/feature/Issues/Issue425Test.php index 096026b17..a061afa92 100644 --- a/tests/feature/Issues/Issue425Test.php +++ b/tests/feature/Issues/Issue425Test.php @@ -16,5 +16,8 @@ function (): void { }, 'reference must be present', '- reference must be present', - ['reference' => 'reference must be present'] + [ + '__root__' => 'These rules must pass for `["age": 1]`', + 'reference' => 'reference must be present', + ] )); diff --git a/tests/feature/Issues/Issue446Test.php b/tests/feature/Issues/Issue446Test.php index c99c54691..64411d6ea 100644 --- a/tests/feature/Issues/Issue446Test.php +++ b/tests/feature/Issues/Issue446Test.php @@ -19,5 +19,8 @@ ->assert($arr), 'The length of name must be between 2 and 32', '- The length of name must be between 2 and 32', - ['name' => 'The length of name must be between 2 and 32'] + [ + '__root__' => 'These rules must pass for `["name": "w", "email": "hello@hello.com"]`', + 'name' => 'The length of name must be between 2 and 32', + ] )); diff --git a/tests/feature/Issues/Issue796Test.php b/tests/feature/Issues/Issue796Test.php index 24519b018..c531577a4 100644 --- a/tests/feature/Issues/Issue796Test.php +++ b/tests/feature/Issues/Issue796Test.php @@ -50,7 +50,13 @@ FULL_MESSAGE, [ '__root__' => 'All the required rules must pass for the given data', - 'mysql' => 'host must be a string', - 'postgresql' => 'user must be a string', + 'mysql' => [ + '__root__' => 'These rules must pass for mysql', + 'host' => 'host must be a string', + ], + 'postgresql' => [ + '__root__' => 'These rules must pass for postgresql', + 'user' => 'user must be a string', + ], ] )); diff --git a/tests/feature/Issues/Issue799Test.php b/tests/feature/Issues/Issue799Test.php index e93c76dbb..5e114c596 100644 --- a/tests/feature/Issues/Issue799Test.php +++ b/tests/feature/Issues/Issue799Test.php @@ -42,5 +42,8 @@ function ($url) { ->assert($input), 'scheme must start with "https"', '- scheme must start with "https"', - ['scheme' => 'scheme must start with "https"'] + [ + '__root__' => 'These rules must pass for `["scheme": "http", "host": "www.google.com", "path": "/search", "query": "q=respect.github.com"]`', + 'scheme' => 'scheme must start with "https"', + ] )); diff --git a/tests/feature/Rules/AllOfTest.php b/tests/feature/Rules/AllOfTest.php index 32e364660..d2069c1b5 100644 --- a/tests/feature/Rules/AllOfTest.php +++ b/tests/feature/Rules/AllOfTest.php @@ -41,7 +41,10 @@ fn() => v::allOf(v::not(v::intType()), v::greaterThan(2))->assert(4), '4 must not be an integer', '- 4 must not be an integer', - ['notIntType' => '4 must not be an integer'] + [ + '__root__' => 'These rules must pass for 4', + 'notIntType' => '4 must not be an integer', + ] )); test('With a single template', expectAll( diff --git a/tests/feature/Rules/AttributesTest.php b/tests/feature/Rules/AttributesTest.php index e853b66a3..7f689f0cc 100644 --- a/tests/feature/Rules/AttributesTest.php +++ b/tests/feature/Rules/AttributesTest.php @@ -13,14 +13,20 @@ fn() => v::attributes()->assert(new WithAttributes('', 'john.doe@gmail.com', '2024-06-23')), 'name must not be empty', '- name must not be empty', - ['name' => 'name must not be empty'] + [ + '__root__' => 'These rules must pass for `Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$email="john.doe@gmail.com" +$birthdate="2024-06-23" + ... }`', + 'name' => 'name must not be empty', + ] )); test('Inverted', expectAll( fn() => v::attributes()->assert(new WithAttributes('John Doe', 'john.doe@gmail.com', '2024-06-23', '+1234567890')), 'phone must be a valid telephone number or must be null', '- phone must be a valid telephone number or must be null', - ['phone' => 'phone must be a valid telephone number or must be null'] + [ + '__root__' => 'These rules must pass for `Respect\Validation\Test\Stubs\WithAttributes { +$name="John Doe" +$email="john.doe@gmail.com" +$birthdate="2024- ... }`', + 'phone' => 'phone must be a valid telephone number or must be null', + ] )); test('Not an object', expectAll( @@ -34,7 +40,10 @@ fn() => v::attributes()->assert(new WithAttributes('John Doe', 'john.doe@gmail.com', '2024-06-23', 'not a phone number')), 'phone must be a valid telephone number or must be null', '- phone must be a valid telephone number or must be null', - ['phone' => 'phone must be a valid telephone number or must be null'] + [ + '__root__' => 'These rules must pass for `Respect\Validation\Test\Stubs\WithAttributes { +$name="John Doe" +$email="john.doe@gmail.com" +$birthdate="2024- ... }`', + 'phone' => 'phone must be a valid telephone number or must be null', + ] )); test('Multiple attributes, all failed', expectAll( diff --git a/tests/feature/Rules/EachTest.php b/tests/feature/Rules/EachTest.php index e63ec8b25..ee1f78249 100644 --- a/tests/feature/Rules/EachTest.php +++ b/tests/feature/Rules/EachTest.php @@ -225,18 +225,18 @@ ], ]) ->assert(['a', 'b', 'c']), - 'Wrapped must be an integer', + 'First item should have been an integer', <<<'FULL_MESSAGE' - - Each item in Wrapped must be valid - - Wrapped must be an integer - - Wrapped must be an integer - - Wrapped must be an integer + - Here a sequence of items that did not pass the validation + - First item should have been an integer + - Second item should have been an integer + - Third item should have been an integer FULL_MESSAGE, [ - '__root__' => 'Each item in Wrapped must be valid', - 0 => 'Wrapped must be an integer', - 1 => 'Wrapped must be an integer', - 2 => 'Wrapped must be an integer', + '__root__' => 'Here a sequence of items that did not pass the validation', + 0 => 'First item should have been an integer', + 1 => 'Second item should have been an integer', + 2 => 'Third item should have been an integer', ] )); @@ -282,8 +282,17 @@ FULL_MESSAGE, [ '__root__' => 'Each item in `[["not_int": "wrong"], ["my_int": 2], "not an array"]` must be valid', - 0 => 'my_int must be present', - 1 => 'my_int must be an odd number', + 0 => [ + '__root__' => 'These rules must pass for `["not_int": "wrong"]`', + 'my_int' => 'my_int must be present', + ], + 1 => [ + '__root__' => 'These rules must pass for `["my_int": 2]`', + 'my_int' => [ + '__root__' => 'These rules must pass for my_int', + 'odd' => 'my_int must be an odd number', + ], + ], 2 => [ '__root__' => 'All the required rules must pass for "not an array"', 'arrayType' => '"not an array" must be an array', diff --git a/tests/feature/Rules/KeySetTest.php b/tests/feature/Rules/KeySetTest.php index 836759cd1..85b7a8f17 100644 --- a/tests/feature/Rules/KeySetTest.php +++ b/tests/feature/Rules/KeySetTest.php @@ -11,21 +11,30 @@ fn() => v::keySet(v::key('foo', v::intType()))->assert(['foo' => 'string']), 'foo must be an integer', '- foo must be an integer', - ['foo' => 'foo must be an integer'] + [ + '__root__' => '`["foo": "string"]` validation failed', + 'foo' => 'foo must be an integer', + ] )); test('one rule / one missing key', expectAll( fn() => v::keySet(v::keyExists('foo'))->assert([]), 'foo must be present', '- foo must be present', - ['foo' => 'foo must be present'] + [ + '__root__' => '`[]` contains missing keys', + 'foo' => 'foo must be present', + ] )); test('one rule / one extra key', expectAll( fn() => v::keySet(v::keyExists('foo'))->assert(['foo' => 42, 'bar' => 'string']), 'bar must not be present', '- bar must not be present', - ['bar' => 'bar must not be present'] + [ + '__root__' => '`["foo": 42, "bar": "string"]` contains extra keys', + 'bar' => 'bar must not be present', + ] )); test('one rule / one extra key / one missing key', expectAll( @@ -108,7 +117,10 @@ fn() => v::keySet(v::keyExists('foo'), v::keyExists('bar'))->assert(['foo' => 42]), 'bar must be present', '- bar must be present', - ['bar' => 'bar must be present'] + [ + '__root__' => '`["foo": 42]` contains missing keys', + 'bar' => 'bar must be present', + ] )); test('multiple rules / all failed', expectAll( @@ -133,7 +145,10 @@ )->assert(['foo' => 42, 'bar' => 'string', 'baz' => true]), 'baz must not be present', '- baz must not be present', - ['baz' => 'baz must not be present'] + [ + '__root__' => '`["foo": 42, "bar": "string", "baz": true]` contains extra keys', + 'baz' => 'baz must not be present', + ] )); test('multiple rules / one extra key / one missing', expectAll( diff --git a/tests/feature/Rules/PropertyTest.php b/tests/feature/Rules/PropertyTest.php index 0833cf719..8b2cd5f2b 100644 --- a/tests/feature/Rules/PropertyTest.php +++ b/tests/feature/Rules/PropertyTest.php @@ -21,6 +21,23 @@ ['foo' => 'foo must be an integer'] )); +test('With multiple rules', expectAll( + fn() => v::property('foo', v::intType()->positive())->assert((object) ['foo' => 'string']), + 'foo must be an integer', + <<<'FULL_MESSAGE' + - All the required rules must pass for foo + - foo must be an integer + - foo must be a positive number + FULL_MESSAGE, + [ + 'foo' => [ + '__root__' => 'All the required rules must pass for foo', + 'intType' => 'foo must be an integer', + 'positive' => 'foo must be a positive number', + ], + ] +)); + test('Inverted', expectAll( fn() => v::not(v::property('foo', v::intType()))->assert((object) ['foo' => 12]), 'foo must not be an integer', diff --git a/tests/library/Builders/ResultBuilder.php b/tests/library/Builders/ResultBuilder.php index d37a9f0b3..21ab87075 100644 --- a/tests/library/Builders/ResultBuilder.php +++ b/tests/library/Builders/ResultBuilder.php @@ -35,8 +35,6 @@ final class ResultBuilder private ?Result $adjacent = null; - private bool $unchangeableId = false; - /** @var array */ private array $children = []; @@ -57,7 +55,7 @@ public function build(): Result $this->name, $this->id, $this->adjacent, - $this->unchangeableId, + null, ...$this->children ); } diff --git a/tests/unit/Message/StandardFormatter/ArrayProvider.php b/tests/unit/Message/StandardFormatter/ArrayProvider.php index 695faa919..80959ca96 100644 --- a/tests/unit/Message/StandardFormatter/ArrayProvider.php +++ b/tests/unit/Message/StandardFormatter/ArrayProvider.php @@ -76,7 +76,10 @@ public static function provideForArray(): array [ '__root__' => '__parent_original__', '1st' => '__1st_original__', - '2nd' => '__2nd_1st_original__', + '2nd' => [ + '__root__' => '__2nd_original__', + '2nd_1st' => '__2nd_1st_original__', + ], '3rd' => '__3rd_original__', ], ],