Skip to content

Commit

Permalink
Move invalid value exception data to dedicated fields (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Aug 29, 2021
1 parent 8dbd73e commit 1d7cce2
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 39 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.12.37] - 2021-08-29

### Added
- `InvalidValue` now exposes `data` and `constraint` values for structured context of validation failure.

### Fixed
- Handling of `multipleOf: 0.01` float precision.

## [0.12.36] - 2021-07-14

### Added
Expand Down Expand Up @@ -93,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Export `null` value instead of skipping it for properties having `null` type.

[0.12.37]: https://github.com/swaggest/php-json-schema/compare/v0.12.36...v0.12.37
[0.12.36]: https://github.com/swaggest/php-json-schema/compare/v0.12.35...v0.12.36
[0.12.35]: https://github.com/swaggest/php-json-schema/compare/v0.12.34...v0.12.35
[0.12.34]: https://github.com/swaggest/php-json-schema/compare/v0.12.33...v0.12.34
Expand Down
23 changes: 23 additions & 0 deletions src/InvalidValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ class InvalidValue extends Exception
public $error;
public $path;

public $constraint;
public $data;

/**
* @param mixed $constraint
* @return $this
*/
public function withConstraint($constraint)
{
$this->constraint = $constraint;
return $this;
}

/**
* @param mixed $data
* @return $this
*/
public function withData($data)
{
$this->data = $data;
return $this;
}

public function addPath($path)
{
if ($this->error === null) {
Expand Down
109 changes: 70 additions & 39 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public function in($data, Context $options = null)
if ($this->__booleanSchema) {
return $data;
} elseif (empty($options->skipValidation)) {
$this->fail(new InvalidValue('Denied by false schema'), '#');
$this->fail((new InvalidValue('Denied by false schema'))->withData($data), '#');
}
}

Expand Down Expand Up @@ -213,10 +213,10 @@ private function processType(&$data, Context $options, $path = '#')
$valid = Type::isValid($this->type, $data, $options->version);
}
if (!$valid) {
$this->fail(new TypeException(ucfirst(
$this->fail((new TypeException(ucfirst(
implode(', ', is_array($this->type) ? $this->type : array($this->type))
. ' expected, ' . json_encode($data) . ' received')
), $path);
))->withData($data)->withConstraint($this->type), $path);
}
}

Expand Down Expand Up @@ -250,7 +250,9 @@ private function processEnum($data, $path = '#')
}
}
if (!$enumOk) {
$this->fail(new EnumException('Enum failed, enum: ' . json_encode($this->enum) . ', data: ' . json_encode($data)), $path);
$this->fail((new EnumException('Enum failed, enum: ' . json_encode($this->enum) . ', data: ' . json_encode($data)))
->withData($data)
->withConstraint($this->enum), $path);
}
}

Expand All @@ -274,10 +276,14 @@ private function processConst($data, $path)
$diff = new JsonDiff($this->const, $data,
JsonDiff::STOP_ON_DIFF);
if ($diff->getDiffCnt() != 0) {
$this->fail(new ConstException('Const failed'), $path);
$this->fail((new ConstException('Const failed'))
->withData($data)
->withConstraint($this->const), $path);
}
} else {
$this->fail(new ConstException('Const failed'), $path);
$this->fail((new ConstException('Const failed'))
->withData($data)
->withConstraint($this->const), $path);
}
}
}
Expand All @@ -299,7 +305,8 @@ private function processNot($data, Context $options, $path)
// Expected exception
}
if ($exception === false) {
$this->fail(new LogicException('Not ' . json_encode($this->not) . ' expected, ' . json_encode($data) . ' received'), $path . '->not');
$this->fail((new LogicException('Not ' . json_encode($this->not) . ' expected, ' . json_encode($data) . ' received'))
->withData($data), $path . '->not');
}
}

Expand All @@ -312,25 +319,29 @@ private function processString($data, $path)
{
if ($this->minLength !== null) {
if (mb_strlen($data, 'UTF-8') < $this->minLength) {
$this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
$this->fail((new StringException('String is too short', StringException::TOO_SHORT))
->withData($data)->withConstraint($this->minLength), $path);
}
}
if ($this->maxLength !== null) {
if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
$this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
$this->fail((new StringException('String is too long', StringException::TOO_LONG))
->withData($data)->withConstraint($this->maxLength), $path);
}
}
if ($this->pattern !== null) {
if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) {
$this->fail(new StringException(json_encode($data) . ' does not match to '
. $this->pattern, StringException::PATTERN_MISMATCH), $path);
$this->fail((new StringException(json_encode($data) . ' does not match to '
. $this->pattern, StringException::PATTERN_MISMATCH))
->withData($data)->withConstraint($this->pattern), $path);
}
}
if ($this->format !== null) {
$validationError = Format::validationError($this->format, $data);
if ($validationError !== null) {
if (!($this->format === "uri" && substr($path, -3) === ':id')) {
$this->fail(new StringException($validationError), $path);
$this->fail((new StringException($validationError))
->withData($data), $path);
}
}
}
Expand All @@ -345,55 +356,62 @@ private function processNumeric($data, $path)
{
if ($this->multipleOf !== null) {
$div = $data / $this->multipleOf;
if ($div != (int)$div) {
$this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
if ($div != (int)$div && ($div = $data * (1 / $this->multipleOf)) && ($div != (int)$div)) {
$this->fail((new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF))
->withData($data)->withConstraint($this->multipleOf), $path);
}
}

if ($this->exclusiveMaximum !== null && !is_bool($this->exclusiveMaximum)) {
if ($data >= $this->exclusiveMaximum) {
$this->fail(new NumericException(
$this->fail((new NumericException(
'Value less or equal than ' . $this->exclusiveMaximum . ' expected, ' . $data . ' received',
NumericException::MAXIMUM), $path);
NumericException::MAXIMUM))
->withData($data)->withConstraint($this->exclusiveMaximum), $path);
}
}

if ($this->exclusiveMinimum !== null && !is_bool($this->exclusiveMinimum)) {
if ($data <= $this->exclusiveMinimum) {
$this->fail(new NumericException(
$this->fail((new NumericException(
'Value more or equal than ' . $this->exclusiveMinimum . ' expected, ' . $data . ' received',
NumericException::MINIMUM), $path);
NumericException::MINIMUM))
->withData($data)->withConstraint($this->exclusiveMinimum), $path);
}
}

if ($this->maximum !== null) {
if ($this->exclusiveMaximum === true) {
if ($data >= $this->maximum) {
$this->fail(new NumericException(
$this->fail((new NumericException(
'Value less or equal than ' . $this->maximum . ' expected, ' . $data . ' received',
NumericException::MAXIMUM), $path);
NumericException::MAXIMUM))
->withData($data)->withConstraint($this->maximum), $path);
}
} else {
if ($data > $this->maximum) {
$this->fail(new NumericException(
$this->fail((new NumericException(
'Value less than ' . $this->maximum . ' expected, ' . $data . ' received',
NumericException::MAXIMUM), $path);
NumericException::MAXIMUM))
->withData($data)->withConstraint($this->maximum), $path);
}
}
}

if ($this->minimum !== null) {
if ($this->exclusiveMinimum === true) {
if ($data <= $this->minimum) {
$this->fail(new NumericException(
$this->fail((new NumericException(
'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
NumericException::MINIMUM), $path);
NumericException::MINIMUM))
->withData($data)->withConstraint($this->minimum), $path);
}
} else {
if ($data < $this->minimum) {
$this->fail(new NumericException(
$this->fail((new NumericException(
'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
NumericException::MINIMUM), $path);
NumericException::MINIMUM))
->withData($data)->withConstraint($this->minimum), $path);
}
}
}
Expand Down Expand Up @@ -445,13 +463,15 @@ private function processOneOf($data, Context $options, $path)
$exception = new LogicException('No valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}");
$exception->error = 'No valid results for oneOf';
$exception->subErrors = $subErrors;
$exception->data = $data;
$this->fail($exception, $path);
} elseif ($successes > 1) {
$exception = new LogicException('More than 1 valid result for oneOf: '
. $successes . '/' . count($this->oneOf) . ' valid results for oneOf {'
. "\n" . substr($failures, 0, -1) . "\n}");
$exception->error = 'More than 1 valid result for oneOf';
$exception->subErrors = $subErrors;
$exception->data = $data;
$this->fail($exception, $path);
}
}
Expand Down Expand Up @@ -492,6 +512,7 @@ private function processAnyOf($data, Context $options, $path)
. "\n}");
$exception->error = 'No valid results for anyOf';
$exception->subErrors = $subErrors;
$exception->data = $data;
$this->fail($exception, $path);
}
return $result;
Expand Down Expand Up @@ -554,7 +575,12 @@ private function processObjectRequired($array, Context $options, $path)
{
foreach ($this->required as $item) {
if (!array_key_exists($item, $array)) {
$this->fail(new ObjectException('Required property missing: ' . $item . ', data: ' . json_encode($array, JSON_UNESCAPED_SLASHES), ObjectException::REQUIRED), $path);
$this->fail(
(new ObjectException(
'Required property missing: ' . $item . ', data: ' . json_encode($array, JSON_UNESCAPED_SLASHES),
ObjectException::REQUIRED))
->withData((object)$array)->withConstraint($item),
$path);
}
}
}
Expand Down Expand Up @@ -780,10 +806,12 @@ private function processObject($data, Context $options, $path, $result = null)

if (!$options->skipValidation) {
if ($this->minProperties !== null && count($array) < $this->minProperties) {
$this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
$this->fail((new ObjectException("Not enough properties", ObjectException::TOO_FEW))
->withData($data)->withConstraint($this->minProperties), $path);
}
if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
$this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
$this->fail((new ObjectException("Too many properties", ObjectException::TOO_MANY))
->withData($data)->withConstraint($this->maxProperties), $path);
}
if ($this->propertyNames !== null) {
$propertyNames = self::unboolSchema($this->propertyNames);
Expand Down Expand Up @@ -845,8 +873,9 @@ private function processObject($data, Context $options, $path, $result = null)
} else {
foreach ($dependencies as $item) {
if (!array_key_exists($item, $array)) {
$this->fail(new ObjectException('Dependency property missing: ' . $item,
ObjectException::DEPENDENCY_MISSING), $path);
$this->fail((new ObjectException('Dependency property missing: ' . $item,
ObjectException::DEPENDENCY_MISSING))
->withData($data)->withConstraint($item), $path);
}
}
}
Expand Down Expand Up @@ -892,7 +921,8 @@ private function processObject($data, Context $options, $path, $result = null)
}
if (!$found && $this->additionalProperties !== null) {
if (!$options->skipValidation && $this->additionalProperties === false) {
$this->fail(new ObjectException('Additional properties not allowed: ' . $key), $path);
$this->fail((new ObjectException('Additional properties not allowed: ' . $key))
->withData($data), $path);
}

if ($this->additionalProperties instanceof SchemaContract) {
Expand Down Expand Up @@ -968,11 +998,11 @@ private function processArray($data, Context $options, $path, $result)
$count = count($data);
if (!$options->skipValidation) {
if ($this->minItems !== null && $count < $this->minItems) {
$this->fail(new ArrayException("Not enough items in array"), $path);
$this->fail((new ArrayException("Not enough items in array"))->withData($data), $path);
}

if ($this->maxItems !== null && $count > $this->maxItems) {
$this->fail(new ArrayException("Too many items in array"), $path);
$this->fail((new ArrayException("Too many items in array"))->withData($data), $path);
}
}

Expand Down Expand Up @@ -1007,26 +1037,26 @@ private function processArray($data, Context $options, $path, $result)
$result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
. '[' . $index . ']:' . $index);
} elseif (!$options->skipValidation && $additionalItems === false) {
$this->fail(new ArrayException('Unexpected array item'), $path);
$this->fail((new ArrayException('Unexpected array item'))->withData($data), $path);
}
}
++$index;
}

if (!$options->skipValidation && $this->uniqueItems) {
if (!UniqueItems::isValid($data)) {
$this->fail(new ArrayException('Array is not unique'), $path);
$this->fail((new ArrayException('Array is not unique'))->withData($data), $path);
}
}

if (!$options->skipValidation && $this->contains !== null) {
/** @var Schema|bool $contains */
$contains = $this->contains;
if ($contains === false) {
$this->fail(new ArrayException('Contains is false'), $path);
$this->fail((new ArrayException('Contains is false'))->withData($data), $path);
}
if ($count === 0) {
$this->fail(new ArrayException('Empty array fails contains constraint'), $path);
$this->fail((new ArrayException('Empty array fails contains constraint')), $path);
}
if ($contains === true) {
$contains = self::unboolSchema($contains);
Expand All @@ -1041,7 +1071,8 @@ private function processArray($data, Context $options, $path, $result)
}
}
if (!$containsOk) {
$this->fail(new ArrayException('Array fails contains constraint'), $path);
$this->fail((new ArrayException('Array fails contains constraint'))
->withData($data), $path);
}
}
return $result;
Expand Down
22 changes: 22 additions & 0 deletions tests/resources/suite/multipleOf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"description": "multiple of with float precision",
"schema": {
"multipleOf": 0.01
},
"tests": [
{
"data": 4.22,
"valid": true
},
{
"data": 4.21,
"valid": true
},
{
"data": 4.215,
"valid": false
}
]
}
]
1 change: 1 addition & 0 deletions tests/src/PHPUnit/Error/ErrorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public function testErrorMessage()
$error = $exception->inspect();
$this->assertSame($errorInspected, print_r($error, 1));
$this->assertSame('/properties/root/patternProperties/^[a-zA-Z0-9_]+$', $exception->getSchemaPointer());
$this->assertSame('f', $exception->data);

// Resolving schema pointer to schema data.
$failedSchemaData = JsonPointer::getByPointer($schemaData, $exception->getSchemaPointer());
Expand Down

0 comments on commit 1d7cce2

Please sign in to comment.