respect-validation/tests/feature/Rules/KeySetTest.php
Henrique Moody 1915b6fff7
Use paths to identify when a rule fails
When nested-structural validation fails, it's challenging to identify
which rule failed from the main exception message. A great example is
the `Issue796Test.php` file. The exception message says:

host must be a string

But you're left unsure whether it's the `host` key from the `mysql` key
or the `postgresql` key.

This commit changes that behaviour by introducing the concept of "Path."
The `path` represents the path that a rule has taken, and we can use it
in structural rules to identify the path of an array or object.

Here's what it looks like before and after:

```diff
-host must be a string
+`.mysql.host` must be a string
```

Because paths are a specific concept, I added a dot (`.`) at the
beginning of all paths when displaying them. I was inspired by the `jq`
syntax. I also added backticks around paths to distinguish them from any
other value.

I didn't manage to fix a test, and I skipped it instead of fixing it
because I want to make changes in how we display error messages as
arrays, and it will be easier to fix it then.
2024-12-27 23:28:35 +01:00

215 lines
7.1 KiB
PHP

<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
test('one rule / one failed', expectAll(
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'],
));
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'],
));
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'],
));
test('one rule / one extra key / one missing key', expectAll(
fn() => v::keySet(v::keyExists('foo'))->assert(['bar' => true]),
'`.foo` must be present',
<<<'FULL_MESSAGE'
- `["bar": true]` contains both missing and extra keys
- `.foo` must be present
- `.bar` must not be present
FULL_MESSAGE,
[
'__root__' => '`["bar": true]` contains both missing and extra keys',
'foo' => '`.foo` must be present',
'bar' => '`.bar` must not be present',
],
));
test('one rule / two extra keys', expectAll(
fn() => v::keySet(v::keyExists('foo'))->assert(['foo' => 42, 'bar' => 'string', 'baz' => true]),
'`.bar` must not be present',
<<<'FULL_MESSAGE'
- `["foo": 42, "bar": "string", "baz": true]` contains extra keys
- `.bar` must not be present
- `.baz` must not be present
FULL_MESSAGE,
[
'__root__' => '`["foo": 42, "bar": "string", "baz": true]` contains extra keys',
'bar' => '`.bar` must not be present',
'baz' => '`.baz` must not be present',
],
));
test('one rule / more than ten extra keys', expectAll(
fn() => v::keySet(v::keyExists('foo'))
->assert([
'foo' => 42,
'bar' => 'string',
'baz' => true,
'qux' => false,
'quux' => 42,
'corge' => 'string',
'grault' => true,
'garply' => false,
'waldo' => 42,
'fred' => 'string',
'plugh' => true,
'xyzzy' => false,
'thud' => 42,
]),
'`.bar` must not be present',
<<<'FULL_MESSAGE'
- `["foo": 42, "bar": "string", "baz": true, "qux": false, "quux": 42, ...]` contains extra keys
- `.bar` must not be present
- `.baz` must not be present
- `.qux` must not be present
- `.quux` must not be present
- `.corge` must not be present
- `.grault` must not be present
- `.garply` must not be present
- `.waldo` must not be present
- `.fred` must not be present
- `.plugh` must not be present
FULL_MESSAGE,
[
'__root__' => '`["foo": 42, "bar": "string", "baz": true, "qux": false, "quux": 42, ...]` contains extra keys',
'bar' => '`.bar` must not be present',
'baz' => '`.baz` must not be present',
'qux' => '`.qux` must not be present',
'quux' => '`.quux` must not be present',
'corge' => '`.corge` must not be present',
'grault' => '`.grault` must not be present',
'garply' => '`.garply` must not be present',
'waldo' => '`.waldo` must not be present',
'fred' => '`.fred` must not be present',
'plugh' => '`.plugh` must not be present',
],
));
test('multiple rules / one failed', expectAll(
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'],
));
test('multiple rules / all failed', expectAll(
fn() => v::keySet(v::keyExists('foo'), v::keyExists('bar'))->assert([]),
'`.foo` must be present',
<<<'FULL_MESSAGE'
- `[]` contains missing keys
- `.foo` must be present
- `.bar` must be present
FULL_MESSAGE,
[
'__root__' => '`[]` contains missing keys',
'foo' => '`.foo` must be present',
'bar' => '`.bar` must be present',
],
));
test('multiple rules / one extra key', expectAll(
fn() => v::keySet(
v::keyExists('foo'),
v::keyExists('bar'),
)->assert(['foo' => 42, 'bar' => 'string', 'baz' => true]),
'`.baz` must not be present',
'- `.baz` must not be present',
['baz' => '`.baz` must not be present'],
));
test('multiple rules / one extra key / one missing', expectAll(
fn() => v::keySet(
v::keyExists('foo'),
v::keyExists('bar'),
)->assert(['bar' => 'string', 'baz' => true]),
'`.foo` must be present',
<<<'FULL_MESSAGE'
- `["bar": "string", "baz": true]` contains both missing and extra keys
- `.foo` must be present
- `.baz` must not be present
FULL_MESSAGE,
[
'__root__' => '`["bar": "string", "baz": true]` contains both missing and extra keys',
'foo' => '`.foo` must be present',
'baz' => '`.baz` must not be present',
],
));
test('multiple rules / two extra keys', expectAll(
fn() => v::keySet(
v::keyExists('foo'),
v::keyExists('bar'),
v::keyOptional('qux', v::intType()),
)->assert(['foo' => 42, 'bar' => 'string', 'baz' => true, 'qux' => false]),
'`.qux` must be an integer',
<<<'FULL_MESSAGE'
- `["foo": 42, "bar": "string", "baz": true, "qux": false]` contains extra keys
- `.qux` must be an integer
- `.baz` must not be present
FULL_MESSAGE,
[
'__root__' => '`["foo": 42, "bar": "string", "baz": true, "qux": false]` contains extra keys',
'qux' => '`.qux` must be an integer',
'baz' => '`.baz` must not be present',
],
));
test('multiple rules / all failed validation', expectAll(
fn() => v::keySet(
v::key('foo', v::intType()),
v::key('bar', v::intType()),
v::key('baz', v::intType()),
)
->assert(['foo' => 42, 'bar' => 'string', 'baz' => true]),
'`.bar` must be an integer',
<<<'FULL_MESSAGE'
- `["foo": 42, "bar": "string", "baz": true]` validation failed
- `.bar` must be an integer
- `.baz` must be an integer
FULL_MESSAGE,
[
'__root__' => '`["foo": 42, "bar": "string", "baz": true]` validation failed',
'bar' => '`.bar` must be an integer',
'baz' => '`.baz` must be an integer',
],
));
test('multiple rules / single missing key / single failed validation', expectAll(
fn() => v::keySet(
v::create()
->key('foo', v::intType())
->key('bar', v::intType())
->key('baz', v::intType()),
)
->assert(['foo' => 42, 'bar' => 'string']),
'`.bar` must be an integer',
<<<'FULL_MESSAGE'
- `["foo": 42, "bar": "string"]` contains missing keys
- `.bar` must be an integer
- `.baz` must be present
FULL_MESSAGE,
[
'__root__' => '`["foo": 42, "bar": "string"]` contains missing keys',
'bar' => '`.bar` must be an integer',
'baz' => '`.baz` must be present',
],
));