Skip to content

Added more precise json/array error messages #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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