ata-validator 1.14.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +27 -2
- package/compat.d.ts +89 -17
- package/compat.js +367 -16
- package/index.d.ts +26 -0
- package/index.js +25 -7
- package/lib/aot-impl.js +18 -0
- package/lib/buffer-gate.js +7 -1
- package/lib/compat-errors.js +343 -0
- package/lib/formats.js +404 -30
- package/lib/interpreter.js +121 -6
- package/lib/js-compiler.js +26 -13
- package/lib/keywords.js +113 -0
- package/lib/plan-compiler.js +34 -2
- package/lib/version.js +1 -1
- package/package.json +11 -9
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Loading is delegated to lib/native-load.js so this file stays free of
|
|
4
4
|
// platform probing and `path` (the browser entry must not pull those in).
|
|
5
5
|
const native = require("./lib/native-load")();
|
|
6
|
+
const { normalizeKeywords, schemaUsesKeywords } = require('./lib/keywords');
|
|
6
7
|
const {
|
|
7
8
|
compileToJS,
|
|
8
9
|
compileToJSCodegen,
|
|
@@ -790,6 +791,16 @@ class Validator {
|
|
|
790
791
|
this._schemaIsCallers = schemaObj === schema;
|
|
791
792
|
this._vocabulariesApplied = false;
|
|
792
793
|
|
|
794
|
+
// Custom keywords, normalized once. `_usesKeywords` is what routes the
|
|
795
|
+
// schema to the interpreted engine and keeps it out of the shared
|
|
796
|
+
// compile cache; a schema that registers keywords but uses none of them
|
|
797
|
+
// takes the ordinary path.
|
|
798
|
+
this._keywords = normalizeKeywords(options.keywords);
|
|
799
|
+
this._usesKeywords = false;
|
|
800
|
+
if (this._keywords !== null && schemaUsesKeywords(schemaObj, this._keywords)) {
|
|
801
|
+
this._usesKeywords = true;
|
|
802
|
+
}
|
|
803
|
+
|
|
793
804
|
this._schemaStr = null; // lazy: computed on first use
|
|
794
805
|
this._schemaObj = schemaObj;
|
|
795
806
|
this._options = options;
|
|
@@ -918,7 +929,7 @@ class Validator {
|
|
|
918
929
|
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
919
930
|
// Custom formats are JS functions: bypass the compile cache since they can
|
|
920
931
|
// differ between validators that share the same schema string.
|
|
921
|
-
const cached = this._userFormats ? null : _compileCache.get(mapKey);
|
|
932
|
+
const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
|
|
922
933
|
let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
|
|
923
934
|
var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
|
|
924
935
|
// v1 removes the bookending requirement for $dynamicRef. Only the
|
|
@@ -936,7 +947,7 @@ class Validator {
|
|
|
936
947
|
// survives the block and would quietly handle schemas it gets wrong; the
|
|
937
948
|
// interpreted engine is both eval-free and more correct, so go straight
|
|
938
949
|
// there.
|
|
939
|
-
if (this._v1Dynamic || !codegenAvailable()) {
|
|
950
|
+
if (this._v1Dynamic || this._usesKeywords || !codegenAvailable()) {
|
|
940
951
|
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
941
952
|
} else if (cached && !_forceNapi) {
|
|
942
953
|
jsFn = cached.jsFn;
|
|
@@ -1030,6 +1041,7 @@ class Validator {
|
|
|
1030
1041
|
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1031
1042
|
formats: this._userFormats,
|
|
1032
1043
|
v1: isV1Dialect(schemaObj),
|
|
1044
|
+
keywords: this._keywords,
|
|
1033
1045
|
});
|
|
1034
1046
|
}
|
|
1035
1047
|
const r = _interp.validate(d);
|
|
@@ -1117,6 +1129,7 @@ class Validator {
|
|
|
1117
1129
|
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1118
1130
|
formats: this._userFormats,
|
|
1119
1131
|
v1: isV1Dialect(schemaObj),
|
|
1132
|
+
keywords: this._keywords,
|
|
1120
1133
|
});
|
|
1121
1134
|
}
|
|
1122
1135
|
const interp = _interp;
|
|
@@ -1323,6 +1336,7 @@ class Validator {
|
|
|
1323
1336
|
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1324
1337
|
formats: this._userFormats,
|
|
1325
1338
|
v1: isV1Dialect(schemaObj),
|
|
1339
|
+
keywords: this._keywords,
|
|
1326
1340
|
});
|
|
1327
1341
|
this._engine = 'interpreter';
|
|
1328
1342
|
_validate = (data) => interp.validate(data);
|
|
@@ -1385,6 +1399,7 @@ class Validator {
|
|
|
1385
1399
|
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1386
1400
|
formats: this._userFormats,
|
|
1387
1401
|
v1: isV1Dialect(schemaObj),
|
|
1402
|
+
keywords: this._keywords,
|
|
1388
1403
|
});
|
|
1389
1404
|
this._engine = 'interpreter';
|
|
1390
1405
|
if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
|
|
@@ -1632,7 +1647,7 @@ class Validator {
|
|
|
1632
1647
|
// every buffer entry point goes through validate() instead.
|
|
1633
1648
|
if (native) {
|
|
1634
1649
|
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
|
|
1635
|
-
if (bufferNeedsSlowPath(schemaObj, this._schemaMap)) installSlowBufferApis(this);
|
|
1650
|
+
if (bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords)) installSlowBufferApis(this);
|
|
1636
1651
|
}
|
|
1637
1652
|
|
|
1638
1653
|
// Save to identity cache for ultra-fast reuse with same schema object
|
|
@@ -1697,7 +1712,7 @@ class Validator {
|
|
|
1697
1712
|
// A validator that rewrites its input cannot use the binding below: that
|
|
1698
1713
|
// one answers from the compiled function alone and would skip the rewrite,
|
|
1699
1714
|
// so isValidObject() and validate() would disagree.
|
|
1700
|
-
if (this._needsPreprocess()) {
|
|
1715
|
+
if (this._needsPreprocess() || this._usesKeywords) {
|
|
1701
1716
|
this._ensureCompiled();
|
|
1702
1717
|
return;
|
|
1703
1718
|
}
|
|
@@ -1708,7 +1723,7 @@ class Validator {
|
|
|
1708
1723
|
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
1709
1724
|
// Custom formats are JS functions: skip the shared cache so different
|
|
1710
1725
|
// validators with the same schema string but different formats don't collide.
|
|
1711
|
-
const cached = this._userFormats ? null : _compileCache.get(mapKey);
|
|
1726
|
+
const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
|
|
1712
1727
|
if (cached && cached.jsFn) {
|
|
1713
1728
|
this._jsFn = cached.jsFn;
|
|
1714
1729
|
this.isValidObject = cached.jsFn;
|
|
@@ -2131,8 +2146,11 @@ _defineLazyMethod('validate', (self) => (data) => {
|
|
|
2131
2146
|
});
|
|
2132
2147
|
_defineLazyMethod('isValidObject', (self) => (data) => {
|
|
2133
2148
|
// A validator that rewrites its input goes through the full compile, which
|
|
2134
|
-
// binds a verdict method that runs the rewrite first.
|
|
2135
|
-
|
|
2149
|
+
// binds a verdict method that runs the rewrite first. So does one whose
|
|
2150
|
+
// schema uses a custom keyword: neither the tier-0 plan nor the code
|
|
2151
|
+
// generator knows the keyword, and either would accept what validate()
|
|
2152
|
+
// rejects.
|
|
2153
|
+
if (self._needsPreprocess() || self._usesKeywords) {
|
|
2136
2154
|
self._ensureCompiled();
|
|
2137
2155
|
return self.isValidObject(data);
|
|
2138
2156
|
}
|
package/lib/aot-impl.js
CHANGED
|
@@ -58,6 +58,9 @@ function safeRePrelude(...fns) {
|
|
|
58
58
|
// Generate a JS module string that can be written to a file.
|
|
59
59
|
// On next startup, load with Validator.fromStandalone() -- zero compile time.
|
|
60
60
|
function toStandalone(validator) {
|
|
61
|
+
if (validator._usesKeywords) {
|
|
62
|
+
throw new Error('toStandalone: a schema that uses custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
63
|
+
}
|
|
61
64
|
validator._ensureCompiled();
|
|
62
65
|
const jsFn = validator._jsFn;
|
|
63
66
|
if (!jsFn || !jsFn._source) return null;
|
|
@@ -219,6 +222,9 @@ function emitClone(node, access, depth) {
|
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
function toStandaloneModule(validator, opts) {
|
|
225
|
+
if (validator._usesKeywords) {
|
|
226
|
+
throw new Error('toStandaloneModule: a schema that uses custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
227
|
+
}
|
|
222
228
|
validator._ensureCompiled();
|
|
223
229
|
const jsFn = validator._jsFn;
|
|
224
230
|
if (!jsFn || !jsFn._source) return null;
|
|
@@ -351,6 +357,9 @@ ${parseCore}${exports}`;
|
|
|
351
357
|
// // On startup:
|
|
352
358
|
// const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
|
|
353
359
|
function bundle(Validator, schemas, opts) {
|
|
360
|
+
if (opts && opts.keywords && Object.keys(opts.keywords).length > 0) {
|
|
361
|
+
throw new Error('bundle: custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
362
|
+
}
|
|
354
363
|
const parts = schemas.map((schema) => {
|
|
355
364
|
const v = new Validator(schema, opts);
|
|
356
365
|
const standalone = toStandalone(v);
|
|
@@ -370,6 +379,12 @@ function bundle(Validator, schemas, opts) {
|
|
|
370
379
|
// opts.format: 'cjs' (default) or 'esm'.
|
|
371
380
|
// opts.formats: { name: fn } — embedded in the output via Function#toString.
|
|
372
381
|
function bundleStandalone(Validator, schemas, opts) {
|
|
382
|
+
// A custom keyword is a function, and a standalone module imports nothing,
|
|
383
|
+
// so there is no way to carry it. Refuse rather than emit a module that
|
|
384
|
+
// silently ignores the keyword.
|
|
385
|
+
if (opts && opts.keywords && Object.keys(opts.keywords).length > 0) {
|
|
386
|
+
throw new Error('bundleStandalone: custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
387
|
+
}
|
|
373
388
|
// Cross-schema $ref resolution: only meaningful when at least one schema has
|
|
374
389
|
// an $id. Skip the schemas-as-map plumbing when none of them do.
|
|
375
390
|
const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
|
|
@@ -436,6 +451,9 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
436
451
|
// Much smaller file → faster V8 parse → faster startup.
|
|
437
452
|
// opts.format: 'cjs' (default) or 'esm'.
|
|
438
453
|
function bundleCompact(Validator, schemas, opts) {
|
|
454
|
+
if (opts && opts.keywords && Object.keys(opts.keywords).length > 0) {
|
|
455
|
+
throw new Error('bundleCompact: custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
456
|
+
}
|
|
439
457
|
const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
|
|
440
458
|
const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
|
|
441
459
|
const format = (opts && opts.format) || 'cjs';
|
package/lib/buffer-gate.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { schemaUsesKeywords } = require('./keywords');
|
|
4
|
+
|
|
3
5
|
// The buffer APIs (isValid, countValid, batchIsValid, isValidNDJSON,
|
|
4
6
|
// isValidParallel, isValidPrepadded, and isValidJSON above the simdjson
|
|
5
7
|
// threshold) answer from the native engine's own walker, which is not the
|
|
@@ -77,7 +79,11 @@ function walk(schema, depth) {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// True when the buffer APIs must answer through validate() for this schema.
|
|
80
|
-
|
|
82
|
+
// `keywords` is the validator's normalized custom keyword map, if any: the
|
|
83
|
+
// native walker knows nothing about those, so a schema using one is routed
|
|
84
|
+
// through validate() like the other shapes here.
|
|
85
|
+
function bufferNeedsSlowPath(schema, schemaMap, keywords) {
|
|
86
|
+
if (keywords && schemaUsesKeywords(schema, keywords)) return true;
|
|
81
87
|
if (walk(schema, 0)) return true;
|
|
82
88
|
if (schemaMap && schemaMap.size > 0) {
|
|
83
89
|
for (const s of schemaMap.values()) if (walk(s, 1)) return true;
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shapes an error list the way the reference validator reports it, for the
|
|
4
|
+
// compat entry. Two things differ between what validate() returns and what
|
|
5
|
+
// `require('ajv')` users read off `validate.errors`:
|
|
6
|
+
//
|
|
7
|
+
// Order. The reference does not follow schema declaration order; it runs
|
|
8
|
+
// keywords in a fixed order per node (the type check, the type-less
|
|
9
|
+
// keywords, then the number, string, array and object groups, each in the
|
|
10
|
+
// order in REFERENCE_ORDER), descending into a subschema when it reaches its
|
|
11
|
+
// applicator. Consumers that show only the first error see that order.
|
|
12
|
+
//
|
|
13
|
+
// Wrapper errors. Where a branch or a subschema fails, the reference reports
|
|
14
|
+
// the branch's own errors and then one for the keyword that combined them:
|
|
15
|
+
// every failing anyOf/oneOf branch, each failing property name under
|
|
16
|
+
// propertyNames, each item under contains (allErrors only), and the `if`
|
|
17
|
+
// keyword after a failed then/else (allErrors only). validate() reports the
|
|
18
|
+
// combining keyword alone. The shaper re-validates the branch or the value
|
|
19
|
+
// against the subschema to recover those errors, so nothing here guesses.
|
|
20
|
+
//
|
|
21
|
+
// With allErrors off the reference stops at the first failing keyword, but
|
|
22
|
+
// that keyword's wrapper group is reported whole: the branch errors and the
|
|
23
|
+
// anyOf, or the property name's error and the propertyNames. The same
|
|
24
|
+
// grouping is applied here after shaping.
|
|
25
|
+
|
|
26
|
+
const REFERENCE_ORDER = [
|
|
27
|
+
'type',
|
|
28
|
+
'$dynamicAnchor', '$dynamicRef', '$recursiveAnchor', '$recursiveRef', '$comment', 'id', '$ref',
|
|
29
|
+
'nullable', 'const', 'enum', 'not', 'anyOf', 'oneOf', 'allOf', 'if', 'then', 'else',
|
|
30
|
+
'maximum', 'minimum', 'exclusiveMaximum', 'exclusiveMinimum', 'multipleOf',
|
|
31
|
+
'maxLength', 'minLength', 'pattern', 'format',
|
|
32
|
+
'maxItems', 'minItems', 'additionalItems', 'prefixItems', 'items', 'contains', 'uniqueItems', 'maxContains', 'minContains', 'unevaluatedItems',
|
|
33
|
+
'maxProperties', 'minProperties', 'required', 'propertyNames', 'additionalProperties', 'dependencies', 'dependentRequired', 'dependentSchemas', 'properties', 'patternProperties', 'unevaluatedProperties',
|
|
34
|
+
];
|
|
35
|
+
const REFERENCE_RANK = new Map(REFERENCE_ORDER.map((k, i) => [k, i]));
|
|
36
|
+
|
|
37
|
+
// Keywords whose next path segment is a user-chosen name or an index rather
|
|
38
|
+
// than a keyword.
|
|
39
|
+
const NAMED_SEGMENT = new Set(['properties', 'patternProperties', 'dependencies', 'dependentSchemas', 'dependentRequired', '$defs', 'definitions', 'propertyDependencies']);
|
|
40
|
+
const INDEXED_SEGMENT = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems', 'items', 'additionalItems']);
|
|
41
|
+
|
|
42
|
+
function unescape(seg) {
|
|
43
|
+
return seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pointerOf(schemaPath) {
|
|
47
|
+
const hash = schemaPath.indexOf('#');
|
|
48
|
+
return hash >= 0 ? schemaPath.slice(hash + 1) : schemaPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function segments(pointer) {
|
|
52
|
+
return pointer === '' ? [] : pointer.split('/').slice(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Walks a pointer's segments and reports, per position, whether that segment
|
|
56
|
+
// is a keyword or a name/index.
|
|
57
|
+
function classify(segs) {
|
|
58
|
+
const kinds = new Array(segs.length);
|
|
59
|
+
let skip = false;
|
|
60
|
+
for (let i = 0; i < segs.length; i++) {
|
|
61
|
+
if (skip) { kinds[i] = 'name'; skip = false; continue; }
|
|
62
|
+
kinds[i] = 'kw';
|
|
63
|
+
const s = segs[i];
|
|
64
|
+
if (NAMED_SEGMENT.has(s)) skip = true;
|
|
65
|
+
else if (INDEXED_SEGMENT.has(s) && /^\d+$/.test(segs[i + 1] || '')) skip = true;
|
|
66
|
+
}
|
|
67
|
+
return kinds;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compareReferenceOrder(a, b) {
|
|
71
|
+
const pa = segments(pointerOf(a.schemaPath || ''));
|
|
72
|
+
const pb = segments(pointerOf(b.schemaPath || ''));
|
|
73
|
+
const ka = classify(pa);
|
|
74
|
+
const n = Math.min(pa.length, pb.length);
|
|
75
|
+
for (let i = 0; i < n; i++) {
|
|
76
|
+
if (pa[i] === pb[i]) continue;
|
|
77
|
+
if (ka[i] !== 'kw') return 0;
|
|
78
|
+
const ra = REFERENCE_RANK.get(pa[i]);
|
|
79
|
+
const rb = REFERENCE_RANK.get(pb[i]);
|
|
80
|
+
if (ra === undefined || rb === undefined) return 0;
|
|
81
|
+
return ra - rb;
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sortLikeReference(errors) {
|
|
87
|
+
return errors.length < 2 ? errors : errors.slice().sort(compareReferenceOrder);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkPointer(node, pointer) {
|
|
91
|
+
for (const raw of segments(pointer)) {
|
|
92
|
+
if (node === null || typeof node !== 'object') return undefined;
|
|
93
|
+
const seg = unescape(raw);
|
|
94
|
+
node = Array.isArray(node) ? node[Number(seg)] : node[seg];
|
|
95
|
+
}
|
|
96
|
+
return node;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function walkData(data, instancePath) {
|
|
100
|
+
return walkPointer(data, instancePath);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function escapePointer(s) {
|
|
104
|
+
return s.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The keywords whose failure the reference reports after the errors of
|
|
108
|
+
// what they combine. Custom keywords with a macro behave the same way and
|
|
109
|
+
// validate() already reports them so.
|
|
110
|
+
const WRAPPERS = new Set(['anyOf', 'oneOf', 'propertyNames', 'contains', 'not', 'if']);
|
|
111
|
+
|
|
112
|
+
function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors, macroKeywords }) {
|
|
113
|
+
const branchCache = new Map();
|
|
114
|
+
const branchValidator = (pointer) => {
|
|
115
|
+
let v = branchCache.get(pointer);
|
|
116
|
+
if (!v) {
|
|
117
|
+
v = new Validator({ $ref: rootId + '#' + pointer }, options);
|
|
118
|
+
branchCache.set(pointer, v);
|
|
119
|
+
}
|
|
120
|
+
return v;
|
|
121
|
+
};
|
|
122
|
+
const wrappers = new Set(WRAPPERS);
|
|
123
|
+
for (const k of macroKeywords) wrappers.add(k);
|
|
124
|
+
|
|
125
|
+
// Errors whose schemaPath is already absolute (from the root), as opposed
|
|
126
|
+
// to relative to the branch validator that produced them.
|
|
127
|
+
const absolute = new WeakSet();
|
|
128
|
+
|
|
129
|
+
// Returns the shaped errors of `value` against the subschema at `pointer`
|
|
130
|
+
// (absolute), with schemaPath made absolute and instancePath rebased onto
|
|
131
|
+
// the caller's.
|
|
132
|
+
function subErrors(pointer, value, schemaPrefix, instancePrefix) {
|
|
133
|
+
const r = branchValidator(pointer).validate(value);
|
|
134
|
+
if (r.valid) return null;
|
|
135
|
+
const shaped = shape(r.errors, value, pointer);
|
|
136
|
+
return shaped.map((e) => {
|
|
137
|
+
const out = {
|
|
138
|
+
...e,
|
|
139
|
+
schemaPath: absolute.has(e) ? e.schemaPath : schemaPrefix + pointer + pointerOf(e.schemaPath || '#'),
|
|
140
|
+
instancePath: instancePrefix + (e.instancePath || ''),
|
|
141
|
+
};
|
|
142
|
+
absolute.add(out);
|
|
143
|
+
return out;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function keywordError(keyword, instancePath, schemaPath, params, message) {
|
|
148
|
+
return { instancePath, schemaPath, keyword, params, message };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// `basePointer` is where `errors` were produced relative to the root
|
|
152
|
+
// document; a branch validator reports paths relative to its branch.
|
|
153
|
+
function shape(errors, data, basePointer) {
|
|
154
|
+
const sorted = sortLikeReference(errors);
|
|
155
|
+
const out = [];
|
|
156
|
+
const consumed = new Set();
|
|
157
|
+
let thenGroup = null; // { prefix, which } for the pending `if` wrapper
|
|
158
|
+
|
|
159
|
+
const flushIf = (e) => {
|
|
160
|
+
if (thenGroup === null) return;
|
|
161
|
+
const segs = segments(pointerOf(e ? e.schemaPath : ''));
|
|
162
|
+
const still = e && segs.length > thenGroup.depth && segs[thenGroup.depth] === thenGroup.which &&
|
|
163
|
+
segs.slice(0, thenGroup.depth).join('/') === thenGroup.prefixSegs;
|
|
164
|
+
if (still) return;
|
|
165
|
+
const ip = thenGroup.instancePath;
|
|
166
|
+
out.push(keywordError('if', ip, thenGroup.schemaPrefix + '/if', { failingKeyword: thenGroup.which }, `must match "${thenGroup.which}" schema`));
|
|
167
|
+
thenGroup = null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (let idx = 0; idx < sorted.length; idx++) {
|
|
171
|
+
const e = sorted[idx];
|
|
172
|
+
if (consumed.has(e)) continue;
|
|
173
|
+
const rel = pointerOf(e.schemaPath || '#');
|
|
174
|
+
const abs = basePointer + rel;
|
|
175
|
+
const segs = segments(rel);
|
|
176
|
+
const kinds = classify(segs);
|
|
177
|
+
const schemaPrefix = (e.schemaPath || '#').slice(0, (e.schemaPath || '#').indexOf('#') + 1);
|
|
178
|
+
|
|
179
|
+
if (allErrors) flushIf(e);
|
|
180
|
+
|
|
181
|
+
// The `if` wrapper: errors from a then/else subschema are followed by
|
|
182
|
+
// one for `if` (allErrors only, the reference returns before it
|
|
183
|
+
// otherwise).
|
|
184
|
+
if (allErrors && thenGroup === null) {
|
|
185
|
+
for (let i = 0; i < segs.length; i++) {
|
|
186
|
+
if (kinds[i] === 'kw' && (segs[i] === 'then' || segs[i] === 'else')) {
|
|
187
|
+
const prefixSegs = segs.slice(0, i);
|
|
188
|
+
thenGroup = {
|
|
189
|
+
depth: i, which: segs[i], prefixSegs: prefixSegs.join('/'),
|
|
190
|
+
schemaPrefix: schemaPrefix + (prefixSegs.length ? '/' + prefixSegs.join('/') : ''),
|
|
191
|
+
instancePath: e.instancePath,
|
|
192
|
+
};
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const kw = e.keyword;
|
|
199
|
+
|
|
200
|
+
// anyOf / oneOf: report each failing branch's errors first.
|
|
201
|
+
if ((kw === 'anyOf' || kw === 'oneOf') && segs[segs.length - 1] === kw) {
|
|
202
|
+
const branches = walkPointer(rootDoc, abs);
|
|
203
|
+
if (Array.isArray(branches)) {
|
|
204
|
+
const value = walkData(data, e.instancePath || '');
|
|
205
|
+
const branchErrors = [];
|
|
206
|
+
const passing = [];
|
|
207
|
+
for (let i = 0; i < branches.length; i++) {
|
|
208
|
+
const errs = subErrors(abs + '/' + i, value, schemaPrefix, e.instancePath || '');
|
|
209
|
+
if (errs === null) passing.push(i);
|
|
210
|
+
else branchErrors.push(allErrors ? errs : errs.slice(0, groupEnd(errs)));
|
|
211
|
+
}
|
|
212
|
+
if (passing.length === 0) {
|
|
213
|
+
for (const errs of branchErrors) out.push(...errs);
|
|
214
|
+
out.push(e);
|
|
215
|
+
} else if (kw === 'oneOf') {
|
|
216
|
+
out.push({ ...e, params: { passingSchemas: passing }, message: 'must match exactly one schema in oneOf' });
|
|
217
|
+
} else {
|
|
218
|
+
out.push(e);
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// propertyNames: the inner errors validate() reports carry no property
|
|
225
|
+
// name. Re-check each name so every failing one is reported with its
|
|
226
|
+
// own errors and a propertyNames error, as the reference does.
|
|
227
|
+
const pnIndex = segs.findIndex((s, i) => kinds[i] === 'kw' && s === 'propertyNames');
|
|
228
|
+
if (pnIndex >= 0 && pnIndex < segs.length - 1) {
|
|
229
|
+
const pnPointer = '/' + segs.slice(0, pnIndex + 1).join('/');
|
|
230
|
+
const pnAbs = basePointer + pnPointer;
|
|
231
|
+
const object = walkData(data, e.instancePath || '');
|
|
232
|
+
if (object && typeof object === 'object' && !Array.isArray(object)) {
|
|
233
|
+
// Everything validate() said about this propertyNames node at this
|
|
234
|
+
// instance is replaced by the regenerated sequence.
|
|
235
|
+
for (let j = idx; j < sorted.length; j++) {
|
|
236
|
+
const o = sorted[j];
|
|
237
|
+
if ((o.instancePath || '') === (e.instancePath || '') && pointerOf(o.schemaPath || '#').startsWith(pnPointer + '/')) consumed.add(o);
|
|
238
|
+
}
|
|
239
|
+
const pnSchemaPath = schemaPrefix + pnPointer;
|
|
240
|
+
for (const name of Object.keys(object)) {
|
|
241
|
+
const errs = subErrors(pnAbs, name, schemaPrefix, e.instancePath || '');
|
|
242
|
+
if (errs === null) continue;
|
|
243
|
+
const kept = allErrors ? errs : errs.slice(0, 1);
|
|
244
|
+
for (const k of kept) out.push({ ...k, instancePath: e.instancePath || '', propertyName: name });
|
|
245
|
+
out.push(keywordError('propertyNames', e.instancePath || '', pnSchemaPath, { propertyName: name }, 'property name must be valid'));
|
|
246
|
+
if (!allErrors) break;
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// contains (allErrors only): each item's errors, then the keyword's.
|
|
253
|
+
if (kw === 'contains' && allErrors && segs[segs.length - 1] === 'contains') {
|
|
254
|
+
const items = walkData(data, e.instancePath || '');
|
|
255
|
+
if (Array.isArray(items)) {
|
|
256
|
+
for (let i = 0; i < items.length; i++) {
|
|
257
|
+
const errs = subErrors(abs, items[i], schemaPrefix, (e.instancePath || '') + '/' + i);
|
|
258
|
+
if (errs !== null) out.push(...errs);
|
|
259
|
+
}
|
|
260
|
+
out.push(e);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// draft-07 `dependencies`, which validate() reads as dependentRequired
|
|
266
|
+
// and dependentSchemas: the reference names the original keyword.
|
|
267
|
+
const depIndex = segs.findIndex((s, i) => kinds[i] === 'kw' && (s === 'dependentRequired' || s === 'dependentSchemas'));
|
|
268
|
+
if (depIndex >= 0) {
|
|
269
|
+
const nodePointer = basePointer + (depIndex === 0 ? '' : '/' + segs.slice(0, depIndex).join('/'));
|
|
270
|
+
const node = walkPointer(rootDoc, nodePointer);
|
|
271
|
+
if (node && typeof node === 'object' && node.dependencies && typeof node.dependencies === 'object') {
|
|
272
|
+
const rewritten = segs.slice();
|
|
273
|
+
rewritten[depIndex] = 'dependencies';
|
|
274
|
+
const schemaPath = schemaPrefix + '/' + rewritten.join('/');
|
|
275
|
+
if (segs[depIndex] === 'dependentRequired' && kw === 'required') {
|
|
276
|
+
const missing = e.params && e.params.missingProperty;
|
|
277
|
+
const object = walkData(data, e.instancePath || '');
|
|
278
|
+
let property;
|
|
279
|
+
for (const [prop, deps] of Object.entries(node.dependencies)) {
|
|
280
|
+
if (Array.isArray(deps) && deps.includes(missing) && object && Object.hasOwn(object, prop)) { property = prop; break; }
|
|
281
|
+
}
|
|
282
|
+
const deps = property !== undefined ? node.dependencies[property] : [missing];
|
|
283
|
+
out.push({
|
|
284
|
+
...e,
|
|
285
|
+
keyword: 'dependencies',
|
|
286
|
+
schemaPath,
|
|
287
|
+
params: { property, missingProperty: missing, depsCount: deps.length, deps: deps.join(', ') },
|
|
288
|
+
message: `must have ${deps.length === 1 ? 'property' : 'properties'} ${deps.join(', ')} when property ${property} is present`,
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
out.push({ ...e, schemaPath });
|
|
292
|
+
}
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
out.push(e);
|
|
298
|
+
}
|
|
299
|
+
if (allErrors) flushIf(null);
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Index just past the first error's wrapper group: the first error, plus
|
|
304
|
+
// the errors that share the path of the outermost wrapper reported later
|
|
305
|
+
// for it (the sibling branches and the anyOf, the name's errors and the
|
|
306
|
+
// propertyNames, a macro's schema errors and the macro).
|
|
307
|
+
function groupEnd(shaped) {
|
|
308
|
+
if (shaped.length < 2) return shaped.length;
|
|
309
|
+
const first = shaped[0];
|
|
310
|
+
const firstPath = pointerOf(first.schemaPath || '#');
|
|
311
|
+
let end = 1;
|
|
312
|
+
let wrapperPath = null;
|
|
313
|
+
for (let i = 1; i < shaped.length; i++) {
|
|
314
|
+
const e = shaped[i];
|
|
315
|
+
const p = pointerOf(e.schemaPath || '#');
|
|
316
|
+
if (!wrappers.has(e.keyword)) continue;
|
|
317
|
+
if (firstPath === p || firstPath.startsWith(p + '/')) {
|
|
318
|
+
if (wrapperPath === null || p.length < wrapperPath.length) { wrapperPath = p; end = i + 1; }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (wrapperPath === null) return 1;
|
|
322
|
+
// Everything up to the wrapper that lives under its path belongs to
|
|
323
|
+
// the group; anything else reported in between is dropped, as the
|
|
324
|
+
// reference never reached it.
|
|
325
|
+
const group = [];
|
|
326
|
+
for (let i = 0; i < end; i++) {
|
|
327
|
+
const e = shaped[i];
|
|
328
|
+
const p = pointerOf(e.schemaPath || '#');
|
|
329
|
+
if (p === wrapperPath || p.startsWith(wrapperPath + '/')) group.push(e);
|
|
330
|
+
}
|
|
331
|
+
shaped.splice(0, shaped.length, ...group, ...shaped.slice(end));
|
|
332
|
+
return group.length;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return function shapeErrors(errors, data) {
|
|
336
|
+
const shaped = shape(errors, data, '');
|
|
337
|
+
if (allErrors) return shaped;
|
|
338
|
+
const n = groupEnd(shaped);
|
|
339
|
+
return shaped.slice(0, n);
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = { createShaper, sortLikeReference, REFERENCE_ORDER };
|