Skip to content

Commit cfd10bb

Browse files
authored
Merge pull request #38 from systopia/handling-of-referenced-data-with-ingored-error
Fix handling of referenced data with ignored error
2 parents 26a5805 + 4bcfe5d commit cfd10bb

17 files changed

+369
-44
lines changed

README.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@ The structure of the `$limitValidation` keyword is:
4141
"keyword": JSON Schema,
4242
"keywordValue": JSON Schema,
4343
"value": JSON Schema,
44-
"propertyName": JSON Schema,
45-
"propertyPath": JSON Schema,
46-
"itemIndex": JSON Schema,
47-
"validate": bool
44+
"calculatedValueUsedViolatedData": boolean|null,
45+
"validate": boolean
4846
}
4947
],
5048
"schema": JSON Schema
@@ -56,11 +54,13 @@ The structure of the `$limitValidation` keyword is:
5654
If the schema at `condition` is matched, limited validation is performed. It is
5755
applied on the keywords at the same depth and the keywords below. If it's not
5856
set, the result of the condition evaluation on a higher level is used. If the
59-
`$limitValidation` keyword is not used at a higher level, `true` is used as
57+
`$limitValidation` keyword is not used at a higher level, `false` is used as
6058
fallback.
6159

62-
Apart from `validate` all properties of a rule have the default value `true`.
63-
The default of `validate` is `false`.
60+
The properties of a rule have the following defaults:
61+
* `keyword`, `keywordValue`, `value`: `true`
62+
* `calculatedValueUsedViolatedData`: `null`
63+
* `validate`: `false`
6464

6565
To the entries in `rules` the [default rules](#default-rules) are always
6666
appended. To prevent the execution of the default rules a rule with just the
@@ -80,7 +80,12 @@ The rule matching is done like this:
8080
* The value of the violated keyword is matched against the schema in `keywordValue`.
8181
* The invalid value is matched against the schema in `value`.
8282

83-
If all schemas are matched, the rule is matched.
83+
All schemas must be matched for a rule to be matched. If
84+
`calculatedValueUsedViolatedData` is not `null`, the value has to be calculated
85+
(with the `$calculate` keyword) and must or must not have used violated data
86+
depending on the actual value of `calculatedValueUsedViolatedData`. Violated
87+
data is used, if the calculation references a value that has a validation error
88+
(including ignored ones).
8489

8590
The keyword `schema` allows to specify a schema that is validated additionally,
8691
if the condition is matched. This allows for example to require some properties
@@ -150,6 +155,9 @@ The default rules are:
150155
]
151156
}
152157
},
158+
{
159+
"calculatedValueUsedViolatedData": true
160+
},
153161
{
154162
"validate": true
155163
}
@@ -162,7 +170,8 @@ The rules mean:
162170
2. No violation error, if the violated keyword is not `type` and the validated value is `false` or `""` (empty string).
163171
3. No violation error, if the validated keyword is one of:
164172
`minLength`, `minItems`, `minContains`, `minProperties`, `required`, `dependentRequired`
165-
4. Every other validation without limitation.
173+
4. No violation error, if value is calculated and calculation used data with violations (including ignored ones).
174+
5. Every other validation without limitation.
166175

167176
## Empty array to object conversion
168177

src/Errors/ErrorCollectorUtil.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,25 @@ public static function setErrorCollector(ValidationContext $context, ErrorCollec
3737
{
3838
$context->setGlobals(['errorCollector' => $errorCollector]);
3939
}
40+
41+
/**
42+
* Returns an error collector for ignored errors when using the
43+
* "$limitValidation" keyword.
44+
*/
45+
public static function getIgnoredErrorCollector(ValidationContext $context): ErrorCollectorInterface
46+
{
47+
if (!isset($context->globals()['ignoredErrorCollector'])) {
48+
self::setIgnoredErrorCollector($context, new ErrorCollector());
49+
}
50+
51+
return $context->globals()['ignoredErrorCollector'];
52+
}
53+
54+
/**
55+
* Sets the error collector for ignored errors when using the "$limitValidation" keyword.
56+
*/
57+
public static function setIgnoredErrorCollector(ValidationContext $context, ErrorCollectorInterface $errorCollector): void
58+
{
59+
$context->setGlobals(['ignoredErrorCollector' => $errorCollector]);
60+
}
4061
}

src/Expression/Variables/JsonPointerVariable.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ public function getValue(ValidationContext $context, int $flags = 0, ?bool &$vio
8080
$path = $this->pointer->absolutePath($context->fullDataPath());
8181
Assertion::notNull($path);
8282

83-
$hasError = ErrorCollectorUtil::getErrorCollector($context)->hasErrorAt($path);
83+
$hasError = ErrorCollectorUtil::getErrorCollector($context)->hasErrorAt($path)
84+
|| ErrorCollectorUtil::getIgnoredErrorCollector($context)->hasErrorAt($path);
8485
if (false === $violated) {
8586
$violated = $hasError;
8687
}

src/Expression/Variables/Variable.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ public static function create($data, SchemaParser $parser): self
6060

6161
/**
6262
* @param bool $violated Will be set to true, if false is given and the
63-
* variable has violated data
63+
* variable has violated data. Ignored errors (when
64+
* using "$limitValidation" keyword are regarded,
65+
* too.)
6466
*
6567
* @return null|mixed
6668
*

src/KeywordValidators/ApplyLimitValidationKeywordValidator.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Opis\JsonSchema\Errors\ValidationError;
2424
use Opis\JsonSchema\KeywordValidators\AbstractKeywordValidator;
2525
use Opis\JsonSchema\ValidationContext;
26+
use Systopia\JsonSchema\Errors\ErrorCollectorUtil;
2627
use Systopia\JsonSchema\LimitValidation\LimitValidationRule;
2728

2829
class ApplyLimitValidationKeywordValidator extends AbstractKeywordValidator
@@ -52,7 +53,13 @@ public function validate(ValidationContext $context): ?ValidationError
5253
private function handleError(ValidationError $error, ValidationContext $context): ?ValidationError
5354
{
5455
if ('' !== $error->keyword()) {
55-
return $this->shouldIgnoreError($error, $context) ? null : $error;
56+
if ($this->shouldIgnoreError($error, $context)) {
57+
ErrorCollectorUtil::getIgnoredErrorCollector($context)->addError($error);
58+
59+
return null;
60+
}
61+
62+
return $error;
5663
}
5764

5865
$subErrors = $error->subErrors();

src/KeywordValidators/CalculateKeywordValidator.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
use Assert\Assertion;
2424
use Opis\JsonSchema\Errors\ValidationError;
25+
use Opis\JsonSchema\JsonPointer;
2526
use Opis\JsonSchema\Keywords\ErrorTrait;
2627
use Opis\JsonSchema\KeywordValidators\AbstractKeywordValidator;
2728
use Opis\JsonSchema\Schema;
@@ -32,6 +33,7 @@
3233
use Systopia\JsonSchema\Expression\Variables\CalculationVariable;
3334
use Systopia\JsonSchema\Keywords\SetValueTrait;
3435
use Systopia\JsonSchema\Translation\ErrorTranslator;
36+
use Systopia\JsonSchema\Util\CalculateUtil;
3537

3638
final class CalculateKeywordValidator extends AbstractKeywordValidator
3739
{
@@ -49,27 +51,35 @@ public function validate(ValidationContext $context): ?ValidationError
4951
{
5052
$calculationVariable = new CalculationVariable($this->calculation);
5153

54+
$violatedDataUsed = false;
55+
5256
try {
53-
$value = $calculationVariable->getValue($context);
57+
$value = $calculationVariable->getValue($context, 0, $violatedDataUsed);
5458
} catch (ReferencedDataHasViolationException|VariableResolveException $e) {
5559
$value = null;
5660
}
5761

62+
CalculateUtil::setCalculatedValueUsedViolatedData(
63+
$context,
64+
JsonPointer::pathToString($context->fullDataPath()),
65+
$violatedDataUsed
66+
);
67+
5868
if (null === $value) {
59-
return $this->handleCalculationFailed($context);
69+
return $this->handleCalculationFailed($context, $violatedDataUsed);
6070
}
6171

6272
$this->setValue($context, static fn () => $value);
6373

6474
return null === $this->next ? null : $this->next->validate($context);
6575
}
6676

67-
private function handleCalculationFailed(ValidationContext $context): ?ValidationError
77+
private function handleCalculationFailed(ValidationContext $context, bool $violatedDataUsed): ?ValidationError
6878
{
6979
$schema = $context->schema();
7080
Assertion::notNull($schema);
7181
$this->unsetValue($context);
72-
if ($this->isRequired($context)) {
82+
if ($this->isRequired($context) && !$violatedDataUsed) {
7383
// "required" is checked before calculation
7484
return $this->error(
7585
$schema,

src/KeywordValidators/LimitValidationKeywordValidator.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Opis\JsonSchema\Errors\ValidationError;
2424
use Opis\JsonSchema\Info\DataInfo;
2525
use Opis\JsonSchema\ValidationContext;
26+
use Systopia\JsonSchema\Errors\ErrorCollectorUtil;
2627
use Systopia\JsonSchema\LimitValidation\LimitValidationRule;
2728
use Systopia\JsonSchema\Util\SchemaUtil;
2829

@@ -57,12 +58,20 @@ public function validate(ValidationContext $context): ?ValidationError
5758

5859
try {
5960
$conditionSchema = SchemaUtil::loadSchema($this->conditionSchema, $context->loader());
60-
$this->conditionMatched = null === SchemaUtil::validateWithoutLimit($conditionSchema, $context);
61+
$errorCollector = ErrorCollectorUtil::getErrorCollector($context);
62+
ErrorCollectorUtil::setErrorCollector($context, clone $errorCollector);
63+
64+
try {
65+
$this->conditionMatched = null === SchemaUtil::validateWithoutLimit($conditionSchema, $context);
66+
} finally {
67+
ErrorCollectorUtil::setErrorCollector($context, $errorCollector);
68+
}
6169

6270
if ($this->conditionMatched) {
71+
// First continue with "normal" validation, so $schema might reference calculated data.
72+
$error = parent::validate($context);
6373
$schema = SchemaUtil::loadSchema($this->schema, $context->loader());
6474
$subSchemaError = SchemaUtil::validateWithoutLimit($schema, $context);
65-
$error = parent::validate($context);
6675

6776
if (null !== $subSchemaError) {
6877
\assert(null !== $context->schema());
@@ -73,7 +82,7 @@ public function validate(ValidationContext $context): ?ValidationError
7382
DataInfo::fromContext($context),
7483
'Data must match schema',
7584
[],
76-
[$subSchemaError, $error]
85+
[$error, $subSchemaError]
7786
);
7887
}
7988

src/Keywords/NoIntersectKeyword.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public function __construct(string $beginPropertyName, string $endPropertyName)
4343

4444
public function validate(ValidationContext $context, Schema $schema): ?ValidationError
4545
{
46-
if (!ErrorCollectorUtil::getErrorCollector($context)->hasErrorAt($context->currentDataPath())) {
46+
if (!ErrorCollectorUtil::getErrorCollector($context)->hasErrorAt($context->currentDataPath())
47+
&& !ErrorCollectorUtil::getIgnoredErrorCollector($context)->hasErrorAt($context->currentDataPath())
48+
) {
4749
/** @var list<\stdClass> $array */
4850
$array = $context->currentData();
4951
usort(

src/Keywords/OrderObjectsKeyword.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function __construct(array $order)
4646
public function validate(ValidationContext $context, Schema $schema): ?ValidationError
4747
{
4848
if (!ErrorCollectorUtil::getErrorCollector($context)->hasErrorAt($context->currentDataPath())
49+
&& !ErrorCollectorUtil::getIgnoredErrorCollector($context)->hasErrorAt($context->currentDataPath())
4950
&& 'array' === $context->currentDataType()
5051
) {
5152
$this->setValue($context, function (array $array) {

src/Keywords/OrderSimpleKeyword.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function __construct(string $direction)
4646
public function validate(ValidationContext $context, Schema $schema): ?ValidationError
4747
{
4848
if (!ErrorCollectorUtil::getErrorCollector($context)->hasErrorAt($context->currentDataPath())
49+
&& !ErrorCollectorUtil::getIgnoredErrorCollector($context)->hasErrorAt($context->currentDataPath())
4950
&& 'array' === $context->currentDataType()
5051
) {
5152
$this->setValue($context, function (array $array) {

src/LimitValidation/LimitValidationRule.php

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
namespace Systopia\JsonSchema\LimitValidation;
2222

2323
use Opis\JsonSchema\Errors\ValidationError;
24+
use Opis\JsonSchema\JsonPointer;
2425
use Opis\JsonSchema\Schema;
2526
use Opis\JsonSchema\ValidationContext;
27+
use Systopia\JsonSchema\Errors\ErrorCollectorUtil;
28+
use Systopia\JsonSchema\Util\CalculateUtil;
2629
use Systopia\JsonSchema\Util\SchemaUtil;
2730

2831
final class LimitValidationRule
@@ -33,17 +36,20 @@ final class LimitValidationRule
3336

3437
private bool|Schema|\stdClass $valueSchema;
3538

39+
private ?bool $calculatedValueUsedViolatedData;
3640
private bool $validate;
3741

38-
public function __construct(
42+
private function __construct(
3943
bool|\stdClass $keywordSchema,
4044
bool|\stdClass $keywordValueSchema,
4145
bool|\stdClass $valueSchema,
46+
?bool $calculatedValueUsedViolatedData,
4247
bool $validate
4348
) {
4449
$this->keywordSchema = $keywordSchema;
4550
$this->keywordValueSchema = $keywordValueSchema;
4651
$this->valueSchema = $valueSchema;
52+
$this->calculatedValueUsedViolatedData = $calculatedValueUsedViolatedData;
4753
$this->validate = $validate;
4854
}
4955

@@ -52,6 +58,7 @@ public function __construct(
5258
* keyword?: \stdClass|bool,
5359
* keywordValue?: \stdClass|bool,
5460
* value?: \stdClass|bool,
61+
* calculatedValueUsedViolatedData?: ?bool,
5562
* validate?: bool,
5663
* } $rule
5764
*/
@@ -61,6 +68,7 @@ public static function create(array $rule): self
6168
$rule['keyword'] ?? true,
6269
$rule['keywordValue'] ?? true,
6370
$rule['value'] ?? true,
71+
$rule['calculatedValueUsedViolatedData'] ?? null,
6472
$rule['validate'] ?? false
6573
);
6674
}
@@ -82,16 +90,38 @@ private function isRuleMatched(ValidationError $error, ValidationContext $contex
8290
$this->valueSchema = SchemaUtil::loadSchema($this->valueSchema, $context->loader());
8391
}
8492

85-
return null === SchemaUtil::validateWithoutLimit(
86-
$this->keywordSchema,
87-
new ValidationContext($error->keyword(), $context->loader())
88-
) && null === SchemaUtil::validateWithoutLimit(
89-
$this->keywordValueSchema,
90-
// @phpstan-ignore property.dynamicName
91-
new ValidationContext($error->schema()->info()->data()->{$error->keyword()}, $context->loader())
92-
) && null === SchemaUtil::validateWithoutLimit(
93-
$this->valueSchema,
94-
new ValidationContext($error->data()->value(), $context->loader())
93+
return null === $this->subValidate($context, $this->keywordSchema, $error->keyword())
94+
&& null === $this->subValidate(
95+
$context,
96+
$this->keywordValueSchema,
97+
// @phpstan-ignore property.dynamicName
98+
$error->schema()->info()->data()->{$error->keyword()}
99+
) && null === $this->subValidate($context, $this->valueSchema, $error->data()->value())
100+
&& (
101+
null === $this->calculatedValueUsedViolatedData
102+
|| $this->calculatedValueUsedViolatedData === CalculateUtil::wasViolatedDataUsedForCalculatedValue(
103+
$context,
104+
JsonPointer::pathToString($error->data()->fullPath())
105+
)
106+
);
107+
}
108+
109+
/**
110+
* @param mixed $data
111+
*/
112+
private function subValidate(ValidationContext $context, Schema $schema, $data): ?ValidationError
113+
{
114+
$newContext = new ValidationContext($data, $context->loader());
115+
// Use cloned error collectors so it is possible to check if referenced data has violations.
116+
ErrorCollectorUtil::setIgnoredErrorCollector(
117+
$newContext,
118+
clone ErrorCollectorUtil::getIgnoredErrorCollector($context)
119+
);
120+
ErrorCollectorUtil::setErrorCollector(
121+
$newContext,
122+
clone ErrorCollectorUtil::getIgnoredErrorCollector($context)
95123
);
124+
125+
return SchemaUtil::validateWithoutLimit($schema, $newContext);
96126
}
97127
}

src/Parsers/KeywordValidators/LimitValidationKeywordParser.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function parse(SchemaInfo $info, SchemaParser $parser, object $shared): ?
6868
} elseif (null !== LimitValidationKeywordValidator::getCurrentInstance()) {
6969
$condition = LimitValidationKeywordValidator::getCurrentInstance()->isConditionMatched();
7070
} else {
71-
$condition = true;
71+
$condition = false;
7272
}
7373

7474
if (!\is_array($limitValidation->rules ?? [])) {
@@ -123,6 +123,9 @@ private function getStandardRules(SchemaParser $parser): array
123123
],
124124
],
125125
]);
126+
$this->standardRules[] = LimitValidationRule::create([
127+
'calculatedValueUsedViolatedData' => true,
128+
]);
126129
$this->standardRules[] = LimitValidationRule::create(
127130
['validate' => true]
128131
);

0 commit comments

Comments
 (0)