Skip to content

Commit e4e59ee

Browse files
authored
Added more precise json/array error messages (#225)
* Added more precise json/array error messages * Fixed psalm type error
1 parent d0a6c43 commit e4e59ee

13 files changed

+907
-355
lines changed

src/Matcher/ArrayMatcher.php

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
use Coduo\PHPMatcher\Backtrace;
88
use Coduo\PHPMatcher\Exception\Exception;
9+
use Coduo\PHPMatcher\Matcher\ArrayMatcher\Diff;
10+
use Coduo\PHPMatcher\Matcher\ArrayMatcher\Difference;
11+
use Coduo\PHPMatcher\Matcher\ArrayMatcher\StringDifference;
12+
use Coduo\PHPMatcher\Matcher\ArrayMatcher\ValuePatternDifference;
913
use Coduo\PHPMatcher\Parser;
1014
use Coduo\ToString\StringConverter;
1115

@@ -36,8 +40,11 @@ final class ArrayMatcher extends Matcher
3640

3741
private Backtrace $backtrace;
3842

43+
private Diff $diff;
44+
3945
public function __construct(ValueMatcher $propertyMatcher, Backtrace $backtrace, Parser $parser)
4046
{
47+
$this->diff = new Diff();
4148
$this->propertyMatcher = $propertyMatcher;
4249
$this->parser = $parser;
4350
$this->backtrace = $backtrace;
@@ -54,8 +61,9 @@ public function match($value, $pattern) : bool
5461
}
5562

5663
if (!\is_array($value)) {
57-
$this->error = \sprintf('%s "%s" is not a valid array.', \gettype($value), new StringConverter($value));
58-
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
64+
$this->addValuePatternDifference($value, $pattern);
65+
66+
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());
5967

6068
return false;
6169
}
@@ -65,7 +73,7 @@ public function match($value, $pattern) : bool
6573
}
6674

6775
if (!$this->iterateMatch($value, $pattern)) {
68-
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
76+
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());
6977

7078
return false;
7179
}
@@ -80,6 +88,20 @@ public function canMatch($pattern) : bool
8088
return \is_array($pattern) || $this->isArrayPattern($pattern);
8189
}
8290

91+
public function getError() : ?string
92+
{
93+
if (!$this->diff->count()) {
94+
return null;
95+
}
96+
97+
return \implode("\n", \array_map(fn (Difference $difference) : string => $difference->format(), $this->diff->all()));
98+
}
99+
100+
public function clearError() : void
101+
{
102+
$this->diff = new Diff();
103+
}
104+
83105
private function isArrayPattern($pattern) : bool
84106
{
85107
if (!\is_string($pattern)) {
@@ -126,7 +148,7 @@ private function iterateMatch(array $values, array $patterns, string $parentPath
126148
continue;
127149
}
128150

129-
if ($this->valueMatchPattern($value, $pattern)) {
151+
if ($this->valueMatchPattern($value, $pattern, $this->formatFullPath($parentPath, $path))) {
130152
continue;
131153
}
132154

@@ -135,7 +157,9 @@ private function iterateMatch(array $values, array $patterns, string $parentPath
135157
}
136158

137159
if ($this->isArrayPattern($pattern)) {
138-
if (!$this->allExpandersMatch($value, $pattern)) {
160+
if (!$this->allExpandersMatch($value, $pattern, $parentPath)) {
161+
$this->addValuePatternDifference($value, $parentPath, $this->formatFullPath($parentPath, $path));
162+
139163
return false;
140164
}
141165

@@ -200,13 +224,15 @@ private function findNotExistingKeys(array $patterns, array $values) : array
200224
}, ARRAY_FILTER_USE_BOTH);
201225
}
202226

203-
private function valueMatchPattern($value, $pattern) : bool
227+
private function valueMatchPattern($value, $pattern, $parentPath) : bool
204228
{
205229
$match = $this->propertyMatcher->canMatch($pattern) &&
206230
$this->propertyMatcher->match($value, $pattern);
207231

208232
if (!$match) {
209-
$this->error = $this->propertyMatcher->getError();
233+
if (!\is_array($value)) {
234+
$this->addValuePatternDifference($value, $pattern, $parentPath);
235+
}
210236
}
211237

212238
return $match;
@@ -230,7 +256,7 @@ private function getValueByPath(array $array, string $path)
230256

231257
private function setMissingElementInError(string $place, string $path) : void
232258
{
233-
$this->error = \sprintf('There is no element under path %s in %s.', $path, $place);
259+
$this->diff = $this->diff->add(new StringDifference(\sprintf('There is no element under path %s in %s.', $path, $place)));
234260
}
235261

236262
private function formatAccessPath($key) : string
@@ -253,13 +279,14 @@ private function shouldSkipValueMatchingFor($lastPattern) : bool
253279
return $lastPattern === self::UNBOUNDED_PATTERN;
254280
}
255281

256-
private function allExpandersMatch($value, $pattern) : bool
282+
private function allExpandersMatch($value, $pattern, $parentPath = '') : bool
257283
{
258284
$typePattern = $this->parser->parse($pattern);
259285

260286
if (!$typePattern->matchExpanders($value)) {
261-
$this->error = $typePattern->getError();
262-
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
287+
$this->addValuePatternDifference($value, $pattern, $parentPath);
288+
289+
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());
263290

264291
return false;
265292
}
@@ -268,4 +295,13 @@ private function allExpandersMatch($value, $pattern) : bool
268295

269296
return true;
270297
}
298+
299+
private function addValuePatternDifference($value, $pattern, string $path = '') : void
300+
{
301+
$this->diff = $this->diff->add(new ValuePatternDifference(
302+
(string) new StringConverter($value),
303+
(string) new StringConverter($pattern),
304+
$path ? $path : 'root'
305+
));
306+
}
271307
}

src/Matcher/ArrayMatcher/Diff.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;
6+
7+
final class Diff
8+
{
9+
/**
10+
* @var Difference[]
11+
*/
12+
private array $differences;
13+
14+
public function __construct(Difference ...$difference)
15+
{
16+
$this->differences = $difference;
17+
}
18+
19+
public function add(Difference $difference) : self
20+
{
21+
return new self(...\array_merge($this->differences, [$difference]));
22+
}
23+
24+
/**
25+
* @return Difference[]
26+
*/
27+
public function all() : array
28+
{
29+
return $this->differences;
30+
}
31+
32+
public function count() : int
33+
{
34+
return \count($this->differences);
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;
4+
5+
interface Difference
6+
{
7+
public function format() : string;
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;
6+
7+
final class StringDifference implements Difference
8+
{
9+
private string $description;
10+
11+
public function __construct(string $description)
12+
{
13+
$this->description = $description;
14+
}
15+
16+
public function format() : string
17+
{
18+
return $this->description;
19+
}
20+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;
6+
7+
final class ValuePatternDifference implements Difference
8+
{
9+
private string $value;
10+
11+
private string $pattern;
12+
13+
private string $path;
14+
15+
public function __construct(string $value, string $pattern, string $path)
16+
{
17+
$this->value = $value;
18+
$this->pattern = $pattern;
19+
$this->path = $path;
20+
}
21+
22+
public function format() : string
23+
{
24+
return "Value \"{$this->value}\" does not match pattern \"{$this->pattern}\" at path: \"{$this->path}\"";
25+
}
26+
}

src/Matcher/ChainMatcher.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Coduo\PHPMatcher\Matcher;
66

77
use Coduo\PHPMatcher\Backtrace;
8+
use Coduo\PHPMatcher\Matcher\Pattern\Assert\Json;
89
use Coduo\PHPMatcher\Value\SingleLineString;
910
use Coduo\ToString\StringConverter;
1011

@@ -19,6 +20,11 @@ final class ChainMatcher extends Matcher
1920
*/
2021
private array $matchers = [];
2122

23+
/**
24+
* @var array<string, string>
25+
*/
26+
private array $matcherErrors;
27+
2228
/**
2329
* @param Backtrace $backtrace
2430
* @param ValueMatcher[] $matchers
@@ -42,16 +48,23 @@ public function match($value, $pattern) : bool
4248
return true;
4349
}
4450

51+
$this->matcherErrors[\get_class($propertyMatcher)] = (string) $propertyMatcher->getError();
4552
$this->error = $propertyMatcher->getError();
4653
}
4754
}
4855

4956
if (!isset($this->error)) {
50-
$this->error = \sprintf(
51-
'Any matcher from chain can\'t match value "%s" to pattern "%s"',
52-
new SingleLineString((string) new StringConverter($value)),
53-
new SingleLineString((string) new StringConverter($pattern))
54-
);
57+
if (\is_array($value) && isset($this->matcherErrors[ArrayMatcher::class])) {
58+
$this->error = $this->matcherErrors[ArrayMatcher::class];
59+
} elseif (Json::isValidPattern($pattern) && isset($this->matcherErrors[JsonMatcher::class])) {
60+
$this->error = $this->matcherErrors[JsonMatcher::class];
61+
} else {
62+
$this->error = \sprintf(
63+
'Any matcher from chain can\'t match value "%s" to pattern "%s"',
64+
new SingleLineString((string) new StringConverter($value)),
65+
new SingleLineString((string) new StringConverter($pattern))
66+
);
67+
}
5568
}
5669

5770
$this->backtrace->matcherFailed($this->matcherName(), $value, $pattern, $this->error);

src/Matcher/JsonMatcher.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
use Coduo\PHPMatcher\Backtrace;
88
use Coduo\PHPMatcher\Matcher\Pattern\Assert\Json;
9-
use Coduo\PHPMatcher\Value\SingleLineString;
10-
use Coduo\ToString\StringConverter;
119

1210
final class JsonMatcher extends Matcher
1311
{
@@ -50,11 +48,7 @@ public function match($value, $pattern) : bool
5048
$match = $this->arrayMatcher->match(\json_decode($value, true), \json_decode($transformedPattern, true));
5149

5250
if (!$match) {
53-
$this->error = \sprintf(
54-
'Value %s does not match pattern %s',
55-
new SingleLineString((string) new StringConverter($value)),
56-
new SingleLineString((string) new StringConverter($transformedPattern))
57-
);
51+
$this->error = $this->arrayMatcher->getError();
5852

5953
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
6054

tests/BacktraceTest/failed_complex_matching_expected_trace.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,13 @@
306306
#306 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (array) failed to match value "false" with "expr(value == true)" pattern
307307
#307 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (array) error: boolean "false" is not a valid string.
308308
#308 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher failed to match value "Array(3)" with "Array(3)" pattern
309-
#309 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher error: boolean "false" is not a valid string.
309+
#309 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"
310310
#310 Matcher Coduo\PHPMatcher\Matcher\JsonMatcher failed to match value "{"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"}" with "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" pattern
311-
#311 Matcher Coduo\PHPMatcher\Matcher\JsonMatcher error: Value {"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"} does not match pattern {"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}
311+
#311 Matcher Coduo\PHPMatcher\Matcher\JsonMatcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"
312312
#312 Matcher Coduo\PHPMatcher\Matcher\XmlMatcher can't match pattern "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}"
313313
#313 Matcher Coduo\PHPMatcher\Matcher\OrMatcher can't match pattern "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}"
314314
#314 Matcher Coduo\PHPMatcher\Matcher\TextMatcher can't match pattern "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}"
315315
#315 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) failed to match value "{"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"}" with "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" pattern
316-
#316 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) error: Value {"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"} does not match pattern {"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}
316+
#316 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"
317317
#317 Matcher Coduo\PHPMatcher\Matcher failed to match value "{"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"}" with "{"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}" pattern
318-
#318 Matcher Coduo\PHPMatcher\Matcher error: Value {"users":[{"id":131,"firstName":"Norbert","lastName":"Orzechowicz","enabled":true,"roles":[]},{"id":132,"firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":false,"roles":["ROLE_DEVELOPER"]}],"prevPage":"http:\/\/example.com\/api\/users\/1?limit=2","nextPage":"http:\/\/example.com\/api\/users\/3?limit=2"} does not match pattern {"users":[{"id":"@integer@","firstName":"Norbert","lastName":"Orzechowicz","enabled":"@boolean@","roles":"@[email protected]()"},{"id":"@integer@","firstName":"Micha\u0142","lastName":"D\u0105browski","enabled":"expr(value == true)","roles":"@array@"}],"prevPage":"@string@","nextPage":"@string@"}
318+
#318 Matcher Coduo\PHPMatcher\Matcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"

0 commit comments

Comments
 (0)