Skip to content

Move invalid value exception data to dedicated fields #128

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 4 commits into from
Aug 29, 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
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