Skip to content

Commit bdcc2f8

Browse files
authored
Merge ef05a96 into 8dbd73e
2 parents 8dbd73e + ef05a96 commit bdcc2f8

File tree

5 files changed

+125
-39
lines changed

5 files changed

+125
-39
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.12.37] - 2021-08-29
8+
9+
### Added
10+
- `InvalidValue` now exposes `data` and `constraint` values for structured context of validation failure.
11+
12+
### Fixed
13+
- Handling of `multipleOf: 0.01` float precision.
14+
715
## [0.12.36] - 2021-07-14
816

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

104+
[0.12.37]: https://github.com/swaggest/php-json-schema/compare/v0.12.36...v0.12.37
96105
[0.12.36]: https://github.com/swaggest/php-json-schema/compare/v0.12.35...v0.12.36
97106
[0.12.35]: https://github.com/swaggest/php-json-schema/compare/v0.12.34...v0.12.35
98107
[0.12.34]: https://github.com/swaggest/php-json-schema/compare/v0.12.33...v0.12.34

src/InvalidValue.php

+23
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,29 @@ class InvalidValue extends Exception
1313
public $error;
1414
public $path;
1515

16+
public $constraint;
17+
public $data;
18+
19+
/**
20+
* @param mixed $constraint
21+
* @return $this
22+
*/
23+
public function withConstraint($constraint)
24+
{
25+
$this->constraint = $constraint;
26+
return $this;
27+
}
28+
29+
/**
30+
* @param mixed $data
31+
* @return $this
32+
*/
33+
public function withData($data)
34+
{
35+
$this->data = $data;
36+
return $this;
37+
}
38+
1639
public function addPath($path)
1740
{
1841
if ($this->error === null) {

src/Schema.php

+70-39
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public function in($data, Context $options = null)
154154
if ($this->__booleanSchema) {
155155
return $data;
156156
} elseif (empty($options->skipValidation)) {
157-
$this->fail(new InvalidValue('Denied by false schema'), '#');
157+
$this->fail((new InvalidValue('Denied by false schema'))->withData($data), '#');
158158
}
159159
}
160160

@@ -213,10 +213,10 @@ private function processType(&$data, Context $options, $path = '#')
213213
$valid = Type::isValid($this->type, $data, $options->version);
214214
}
215215
if (!$valid) {
216-
$this->fail(new TypeException(ucfirst(
216+
$this->fail((new TypeException(ucfirst(
217217
implode(', ', is_array($this->type) ? $this->type : array($this->type))
218218
. ' expected, ' . json_encode($data) . ' received')
219-
), $path);
219+
))->withData($data)->withConstraint($this->type), $path);
220220
}
221221
}
222222

@@ -250,7 +250,9 @@ private function processEnum($data, $path = '#')
250250
}
251251
}
252252
if (!$enumOk) {
253-
$this->fail(new EnumException('Enum failed, enum: ' . json_encode($this->enum) . ', data: ' . json_encode($data)), $path);
253+
$this->fail((new EnumException('Enum failed, enum: ' . json_encode($this->enum) . ', data: ' . json_encode($data)))
254+
->withData($data)
255+
->withConstraint($this->enum), $path);
254256
}
255257
}
256258

@@ -274,10 +276,14 @@ private function processConst($data, $path)
274276
$diff = new JsonDiff($this->const, $data,
275277
JsonDiff::STOP_ON_DIFF);
276278
if ($diff->getDiffCnt() != 0) {
277-
$this->fail(new ConstException('Const failed'), $path);
279+
$this->fail((new ConstException('Const failed'))
280+
->withData($data)
281+
->withConstraint($this->const), $path);
278282
}
279283
} else {
280-
$this->fail(new ConstException('Const failed'), $path);
284+
$this->fail((new ConstException('Const failed'))
285+
->withData($data)
286+
->withConstraint($this->const), $path);
281287
}
282288
}
283289
}
@@ -299,7 +305,8 @@ private function processNot($data, Context $options, $path)
299305
// Expected exception
300306
}
301307
if ($exception === false) {
302-
$this->fail(new LogicException('Not ' . json_encode($this->not) . ' expected, ' . json_encode($data) . ' received'), $path . '->not');
308+
$this->fail((new LogicException('Not ' . json_encode($this->not) . ' expected, ' . json_encode($data) . ' received'))
309+
->withData($data), $path . '->not');
303310
}
304311
}
305312

@@ -312,25 +319,29 @@ private function processString($data, $path)
312319
{
313320
if ($this->minLength !== null) {
314321
if (mb_strlen($data, 'UTF-8') < $this->minLength) {
315-
$this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
322+
$this->fail((new StringException('String is too short', StringException::TOO_SHORT))
323+
->withData($data)->withConstraint($this->minLength), $path);
316324
}
317325
}
318326
if ($this->maxLength !== null) {
319327
if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
320-
$this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
328+
$this->fail((new StringException('String is too long', StringException::TOO_LONG))
329+
->withData($data)->withConstraint($this->maxLength), $path);
321330
}
322331
}
323332
if ($this->pattern !== null) {
324333
if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) {
325-
$this->fail(new StringException(json_encode($data) . ' does not match to '
326-
. $this->pattern, StringException::PATTERN_MISMATCH), $path);
334+
$this->fail((new StringException(json_encode($data) . ' does not match to '
335+
. $this->pattern, StringException::PATTERN_MISMATCH))
336+
->withData($data)->withConstraint($this->pattern), $path);
327337
}
328338
}
329339
if ($this->format !== null) {
330340
$validationError = Format::validationError($this->format, $data);
331341
if ($validationError !== null) {
332342
if (!($this->format === "uri" && substr($path, -3) === ':id')) {
333-
$this->fail(new StringException($validationError), $path);
343+
$this->fail((new StringException($validationError))
344+
->withData($data), $path);
334345
}
335346
}
336347
}
@@ -345,55 +356,62 @@ private function processNumeric($data, $path)
345356
{
346357
if ($this->multipleOf !== null) {
347358
$div = $data / $this->multipleOf;
348-
if ($div != (int)$div) {
349-
$this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
359+
if ($div != (int)$div && ($div = $data * (1 / $this->multipleOf)) && ($div != (int)$div)) {
360+
$this->fail((new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF))
361+
->withData($data)->withConstraint($this->multipleOf), $path);
350362
}
351363
}
352364

353365
if ($this->exclusiveMaximum !== null && !is_bool($this->exclusiveMaximum)) {
354366
if ($data >= $this->exclusiveMaximum) {
355-
$this->fail(new NumericException(
367+
$this->fail((new NumericException(
356368
'Value less or equal than ' . $this->exclusiveMaximum . ' expected, ' . $data . ' received',
357-
NumericException::MAXIMUM), $path);
369+
NumericException::MAXIMUM))
370+
->withData($data)->withConstraint($this->exclusiveMaximum), $path);
358371
}
359372
}
360373

361374
if ($this->exclusiveMinimum !== null && !is_bool($this->exclusiveMinimum)) {
362375
if ($data <= $this->exclusiveMinimum) {
363-
$this->fail(new NumericException(
376+
$this->fail((new NumericException(
364377
'Value more or equal than ' . $this->exclusiveMinimum . ' expected, ' . $data . ' received',
365-
NumericException::MINIMUM), $path);
378+
NumericException::MINIMUM))
379+
->withData($data)->withConstraint($this->exclusiveMinimum), $path);
366380
}
367381
}
368382

369383
if ($this->maximum !== null) {
370384
if ($this->exclusiveMaximum === true) {
371385
if ($data >= $this->maximum) {
372-
$this->fail(new NumericException(
386+
$this->fail((new NumericException(
373387
'Value less or equal than ' . $this->maximum . ' expected, ' . $data . ' received',
374-
NumericException::MAXIMUM), $path);
388+
NumericException::MAXIMUM))
389+
->withData($data)->withConstraint($this->maximum), $path);
375390
}
376391
} else {
377392
if ($data > $this->maximum) {
378-
$this->fail(new NumericException(
393+
$this->fail((new NumericException(
379394
'Value less than ' . $this->maximum . ' expected, ' . $data . ' received',
380-
NumericException::MAXIMUM), $path);
395+
NumericException::MAXIMUM))
396+
->withData($data)->withConstraint($this->maximum), $path);
381397
}
382398
}
383399
}
384400

385401
if ($this->minimum !== null) {
386402
if ($this->exclusiveMinimum === true) {
387403
if ($data <= $this->minimum) {
388-
$this->fail(new NumericException(
404+
$this->fail((new NumericException(
389405
'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
390-
NumericException::MINIMUM), $path);
406+
NumericException::MINIMUM))
407+
->withData($data)->withConstraint($this->minimum), $path);
391408
}
392409
} else {
393410
if ($data < $this->minimum) {
394-
$this->fail(new NumericException(
411+
$this->fail((new NumericException(
395412
'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
396-
NumericException::MINIMUM), $path);
413+
NumericException::MINIMUM))
414+
->withData($data)->withConstraint($this->minimum), $path);
397415
}
398416
}
399417
}
@@ -445,13 +463,15 @@ private function processOneOf($data, Context $options, $path)
445463
$exception = new LogicException('No valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}");
446464
$exception->error = 'No valid results for oneOf';
447465
$exception->subErrors = $subErrors;
466+
$exception->data = $data;
448467
$this->fail($exception, $path);
449468
} elseif ($successes > 1) {
450469
$exception = new LogicException('More than 1 valid result for oneOf: '
451470
. $successes . '/' . count($this->oneOf) . ' valid results for oneOf {'
452471
. "\n" . substr($failures, 0, -1) . "\n}");
453472
$exception->error = 'More than 1 valid result for oneOf';
454473
$exception->subErrors = $subErrors;
474+
$exception->data = $data;
455475
$this->fail($exception, $path);
456476
}
457477
}
@@ -492,6 +512,7 @@ private function processAnyOf($data, Context $options, $path)
492512
. "\n}");
493513
$exception->error = 'No valid results for anyOf';
494514
$exception->subErrors = $subErrors;
515+
$exception->data = $data;
495516
$this->fail($exception, $path);
496517
}
497518
return $result;
@@ -554,7 +575,12 @@ private function processObjectRequired($array, Context $options, $path)
554575
{
555576
foreach ($this->required as $item) {
556577
if (!array_key_exists($item, $array)) {
557-
$this->fail(new ObjectException('Required property missing: ' . $item . ', data: ' . json_encode($array, JSON_UNESCAPED_SLASHES), ObjectException::REQUIRED), $path);
578+
$this->fail(
579+
(new ObjectException(
580+
'Required property missing: ' . $item . ', data: ' . json_encode($array, JSON_UNESCAPED_SLASHES),
581+
ObjectException::REQUIRED))
582+
->withData((object)$array)->withConstraint($item),
583+
$path);
558584
}
559585
}
560586
}
@@ -780,10 +806,12 @@ private function processObject($data, Context $options, $path, $result = null)
780806

781807
if (!$options->skipValidation) {
782808
if ($this->minProperties !== null && count($array) < $this->minProperties) {
783-
$this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
809+
$this->fail((new ObjectException("Not enough properties", ObjectException::TOO_FEW))
810+
->withData($data)->withConstraint($this->minProperties), $path);
784811
}
785812
if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
786-
$this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
813+
$this->fail((new ObjectException("Too many properties", ObjectException::TOO_MANY))
814+
->withData($data)->withConstraint($this->maxProperties), $path);
787815
}
788816
if ($this->propertyNames !== null) {
789817
$propertyNames = self::unboolSchema($this->propertyNames);
@@ -845,8 +873,9 @@ private function processObject($data, Context $options, $path, $result = null)
845873
} else {
846874
foreach ($dependencies as $item) {
847875
if (!array_key_exists($item, $array)) {
848-
$this->fail(new ObjectException('Dependency property missing: ' . $item,
849-
ObjectException::DEPENDENCY_MISSING), $path);
876+
$this->fail((new ObjectException('Dependency property missing: ' . $item,
877+
ObjectException::DEPENDENCY_MISSING))
878+
->withData($data)->withConstraint($item), $path);
850879
}
851880
}
852881
}
@@ -892,7 +921,8 @@ private function processObject($data, Context $options, $path, $result = null)
892921
}
893922
if (!$found && $this->additionalProperties !== null) {
894923
if (!$options->skipValidation && $this->additionalProperties === false) {
895-
$this->fail(new ObjectException('Additional properties not allowed: ' . $key), $path);
924+
$this->fail((new ObjectException('Additional properties not allowed: ' . $key))
925+
->withData($data), $path);
896926
}
897927

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

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

@@ -1007,26 +1037,26 @@ private function processArray($data, Context $options, $path, $result)
10071037
$result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
10081038
. '[' . $index . ']:' . $index);
10091039
} elseif (!$options->skipValidation && $additionalItems === false) {
1010-
$this->fail(new ArrayException('Unexpected array item'), $path);
1040+
$this->fail((new ArrayException('Unexpected array item'))->withData($data), $path);
10111041
}
10121042
}
10131043
++$index;
10141044
}
10151045

10161046
if (!$options->skipValidation && $this->uniqueItems) {
10171047
if (!UniqueItems::isValid($data)) {
1018-
$this->fail(new ArrayException('Array is not unique'), $path);
1048+
$this->fail((new ArrayException('Array is not unique'))->withData($data), $path);
10191049
}
10201050
}
10211051

10221052
if (!$options->skipValidation && $this->contains !== null) {
10231053
/** @var Schema|bool $contains */
10241054
$contains = $this->contains;
10251055
if ($contains === false) {
1026-
$this->fail(new ArrayException('Contains is false'), $path);
1056+
$this->fail((new ArrayException('Contains is false'))->withData($data), $path);
10271057
}
10281058
if ($count === 0) {
1029-
$this->fail(new ArrayException('Empty array fails contains constraint'), $path);
1059+
$this->fail((new ArrayException('Empty array fails contains constraint')), $path);
10301060
}
10311061
if ($contains === true) {
10321062
$contains = self::unboolSchema($contains);
@@ -1041,7 +1071,8 @@ private function processArray($data, Context $options, $path, $result)
10411071
}
10421072
}
10431073
if (!$containsOk) {
1044-
$this->fail(new ArrayException('Array fails contains constraint'), $path);
1074+
$this->fail((new ArrayException('Array fails contains constraint'))
1075+
->withData($data), $path);
10451076
}
10461077
}
10471078
return $result;

tests/resources/suite/multipleOf.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[
2+
{
3+
"description": "multiple of with float precision",
4+
"schema": {
5+
"multipleOf": 0.01
6+
},
7+
"tests": [
8+
{
9+
"data": 4.22,
10+
"valid": true
11+
},
12+
{
13+
"data": 4.21,
14+
"valid": true
15+
},
16+
{
17+
"data": 4.215,
18+
"valid": false
19+
}
20+
]
21+
}
22+
]

tests/src/PHPUnit/Error/ErrorTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ public function testErrorMessage()
175175
$error = $exception->inspect();
176176
$this->assertSame($errorInspected, print_r($error, 1));
177177
$this->assertSame('/properties/root/patternProperties/^[a-zA-Z0-9_]+$', $exception->getSchemaPointer());
178+
$this->assertSame('f', $exception->data);
178179

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

0 commit comments

Comments
 (0)