Skip to content

Commit

Permalink
Added more precise json/array error messages (#225)
Browse files Browse the repository at this point in the history
* Added more precise json/array error messages

* Fixed psalm type error
  • Loading branch information
norberttech authored Feb 25, 2021
1 parent d0a6c43 commit e4e59ee
Show file tree
Hide file tree
Showing 13 changed files with 907 additions and 355 deletions.
58 changes: 47 additions & 11 deletions src/Matcher/ArrayMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

use Coduo\PHPMatcher\Backtrace;
use Coduo\PHPMatcher\Exception\Exception;
use Coduo\PHPMatcher\Matcher\ArrayMatcher\Diff;
use Coduo\PHPMatcher\Matcher\ArrayMatcher\Difference;
use Coduo\PHPMatcher\Matcher\ArrayMatcher\StringDifference;
use Coduo\PHPMatcher\Matcher\ArrayMatcher\ValuePatternDifference;
use Coduo\PHPMatcher\Parser;
use Coduo\ToString\StringConverter;

Expand Down Expand Up @@ -36,8 +40,11 @@ final class ArrayMatcher extends Matcher

private Backtrace $backtrace;

private Diff $diff;

public function __construct(ValueMatcher $propertyMatcher, Backtrace $backtrace, Parser $parser)
{
$this->diff = new Diff();
$this->propertyMatcher = $propertyMatcher;
$this->parser = $parser;
$this->backtrace = $backtrace;
Expand All @@ -54,8 +61,9 @@ public function match($value, $pattern) : bool
}

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

$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());

return false;
}
Expand All @@ -65,7 +73,7 @@ public function match($value, $pattern) : bool
}

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

return false;
}
Expand All @@ -80,6 +88,20 @@ public function canMatch($pattern) : bool
return \is_array($pattern) || $this->isArrayPattern($pattern);
}

public function getError() : ?string
{
if (!$this->diff->count()) {
return null;
}

return \implode("\n", \array_map(fn (Difference $difference) : string => $difference->format(), $this->diff->all()));
}

public function clearError() : void
{
$this->diff = new Diff();
}

private function isArrayPattern($pattern) : bool
{
if (!\is_string($pattern)) {
Expand Down Expand Up @@ -126,7 +148,7 @@ private function iterateMatch(array $values, array $patterns, string $parentPath
continue;
}

if ($this->valueMatchPattern($value, $pattern)) {
if ($this->valueMatchPattern($value, $pattern, $this->formatFullPath($parentPath, $path))) {
continue;
}

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

if ($this->isArrayPattern($pattern)) {
if (!$this->allExpandersMatch($value, $pattern)) {
if (!$this->allExpandersMatch($value, $pattern, $parentPath)) {
$this->addValuePatternDifference($value, $parentPath, $this->formatFullPath($parentPath, $path));

return false;
}

Expand Down Expand Up @@ -200,13 +224,15 @@ private function findNotExistingKeys(array $patterns, array $values) : array
}, ARRAY_FILTER_USE_BOTH);
}

private function valueMatchPattern($value, $pattern) : bool
private function valueMatchPattern($value, $pattern, $parentPath) : bool
{
$match = $this->propertyMatcher->canMatch($pattern) &&
$this->propertyMatcher->match($value, $pattern);

if (!$match) {
$this->error = $this->propertyMatcher->getError();
if (!\is_array($value)) {
$this->addValuePatternDifference($value, $pattern, $parentPath);
}
}

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

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

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

private function allExpandersMatch($value, $pattern) : bool
private function allExpandersMatch($value, $pattern, $parentPath = '') : bool
{
$typePattern = $this->parser->parse($pattern);

if (!$typePattern->matchExpanders($value)) {
$this->error = $typePattern->getError();
$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
$this->addValuePatternDifference($value, $pattern, $parentPath);

$this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());

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

return true;
}

private function addValuePatternDifference($value, $pattern, string $path = '') : void
{
$this->diff = $this->diff->add(new ValuePatternDifference(
(string) new StringConverter($value),
(string) new StringConverter($pattern),
$path ? $path : 'root'
));
}
}
36 changes: 36 additions & 0 deletions src/Matcher/ArrayMatcher/Diff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;

final class Diff
{
/**
* @var Difference[]
*/
private array $differences;

public function __construct(Difference ...$difference)
{
$this->differences = $difference;
}

public function add(Difference $difference) : self
{
return new self(...\array_merge($this->differences, [$difference]));
}

/**
* @return Difference[]
*/
public function all() : array
{
return $this->differences;
}

public function count() : int
{
return \count($this->differences);
}
}
8 changes: 8 additions & 0 deletions src/Matcher/ArrayMatcher/Difference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types=1);

namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;

interface Difference
{
public function format() : string;
}
20 changes: 20 additions & 0 deletions src/Matcher/ArrayMatcher/StringDifference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;

final class StringDifference implements Difference
{
private string $description;

public function __construct(string $description)
{
$this->description = $description;
}

public function format() : string
{
return $this->description;
}
}
26 changes: 26 additions & 0 deletions src/Matcher/ArrayMatcher/ValuePatternDifference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Coduo\PHPMatcher\Matcher\ArrayMatcher;

final class ValuePatternDifference implements Difference
{
private string $value;

private string $pattern;

private string $path;

public function __construct(string $value, string $pattern, string $path)
{
$this->value = $value;
$this->pattern = $pattern;
$this->path = $path;
}

public function format() : string
{
return "Value \"{$this->value}\" does not match pattern \"{$this->pattern}\" at path: \"{$this->path}\"";
}
}
23 changes: 18 additions & 5 deletions src/Matcher/ChainMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Coduo\PHPMatcher\Matcher;

use Coduo\PHPMatcher\Backtrace;
use Coduo\PHPMatcher\Matcher\Pattern\Assert\Json;
use Coduo\PHPMatcher\Value\SingleLineString;
use Coduo\ToString\StringConverter;

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

/**
* @var array<string, string>
*/
private array $matcherErrors;

/**
* @param Backtrace $backtrace
* @param ValueMatcher[] $matchers
Expand All @@ -42,16 +48,23 @@ public function match($value, $pattern) : bool
return true;
}

$this->matcherErrors[\get_class($propertyMatcher)] = (string) $propertyMatcher->getError();
$this->error = $propertyMatcher->getError();
}
}

if (!isset($this->error)) {
$this->error = \sprintf(
'Any matcher from chain can\'t match value "%s" to pattern "%s"',
new SingleLineString((string) new StringConverter($value)),
new SingleLineString((string) new StringConverter($pattern))
);
if (\is_array($value) && isset($this->matcherErrors[ArrayMatcher::class])) {
$this->error = $this->matcherErrors[ArrayMatcher::class];
} elseif (Json::isValidPattern($pattern) && isset($this->matcherErrors[JsonMatcher::class])) {
$this->error = $this->matcherErrors[JsonMatcher::class];
} else {
$this->error = \sprintf(
'Any matcher from chain can\'t match value "%s" to pattern "%s"',
new SingleLineString((string) new StringConverter($value)),
new SingleLineString((string) new StringConverter($pattern))
);
}
}

$this->backtrace->matcherFailed($this->matcherName(), $value, $pattern, $this->error);
Expand Down
8 changes: 1 addition & 7 deletions src/Matcher/JsonMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

use Coduo\PHPMatcher\Backtrace;
use Coduo\PHPMatcher\Matcher\Pattern\Assert\Json;
use Coduo\PHPMatcher\Value\SingleLineString;
use Coduo\ToString\StringConverter;

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

if (!$match) {
$this->error = \sprintf(
'Value %s does not match pattern %s',
new SingleLineString((string) new StringConverter($value)),
new SingleLineString((string) new StringConverter($transformedPattern))
);
$this->error = $this->arrayMatcher->getError();

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,13 @@
#306 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (array) failed to match value "false" with "expr(value == true)" pattern
#307 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (array) error: boolean "false" is not a valid string.
#308 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher failed to match value "Array(3)" with "Array(3)" pattern
#309 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher error: boolean "false" is not a valid string.
#309 Matcher Coduo\PHPMatcher\Matcher\ArrayMatcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"
#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 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 Matcher Coduo\PHPMatcher\Matcher\JsonMatcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"
#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@"}"
#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@"}"
#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@"}"
#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 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 Matcher Coduo\PHPMatcher\Matcher\ChainMatcher (all) error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"
#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 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 Matcher Coduo\PHPMatcher\Matcher error: Value "false" does not match pattern "expr(value == true)" at path: "[users][1][enabled]"
Loading

0 comments on commit e4e59ee

Please sign in to comment.