ata-validator 1.51.0 → 1.53.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/README.md +26 -20
- package/compat.js +314 -29
- package/index.bundle.mjs +15 -0
- package/lib/aot-impl.js +2 -2
- package/lib/combined-runtime.js +4 -1
- package/lib/compat-errors.js +131 -17
- package/lib/draft7.js +65 -35
- package/lib/error-runtime.js +5 -1
- package/lib/interpreter-core.js +297 -14
- package/lib/js-compiler-errors.js +15 -15
- package/lib/js-compiler.js +196 -45
- package/lib/keywords.js +24 -1
- package/lib/levenshtein.js +21 -5
- package/lib/plan-compiler.js +8 -3
- package/lib/safe-regex.js +33 -14
- package/lib/schema-scan.js +29 -1
- package/lib/validator-core.js +99 -13
- package/lib/version.js +1 -1
- package/package.json +10 -8
package/lib/compat-errors.js
CHANGED
|
@@ -31,6 +31,8 @@ const REFERENCE_ORDER = [
|
|
|
31
31
|
'maxLength', 'minLength', 'pattern', 'format',
|
|
32
32
|
'maxItems', 'minItems', 'additionalItems', 'prefixItems', 'items', 'contains', 'uniqueItems', 'maxContains', 'minContains', 'unevaluatedItems',
|
|
33
33
|
'maxProperties', 'minProperties', 'required', 'propertyNames', 'additionalProperties', 'dependencies', 'dependentRequired', 'dependentSchemas', 'properties', 'patternProperties', 'unevaluatedProperties',
|
|
34
|
+
// The reference's `discriminator` option adds the keyword last.
|
|
35
|
+
'discriminator',
|
|
34
36
|
];
|
|
35
37
|
const REFERENCE_RANK = new Map(REFERENCE_ORDER.map((k, i) => [k, i]));
|
|
36
38
|
|
|
@@ -78,13 +80,26 @@ function classify(segs) {
|
|
|
78
80
|
// segments, and each segment's kind. Everything below used to re-derive these
|
|
79
81
|
// wherever it needed them, which for one shaped rejection meant splitting the
|
|
80
82
|
// same pointer three times per error.
|
|
83
|
+
// Memoized by schemaPath: a schema reports a fixed set of paths, and a
|
|
84
|
+
// rejection reshaped every error's path from scratch, which was a third of the
|
|
85
|
+
// compat error path. The key is read, never written, by its users. Bounded so
|
|
86
|
+
// a schema with paths that grow with the data (recursion) cannot fill memory.
|
|
87
|
+
const _keys = new Map();
|
|
81
88
|
function keyFor(e) {
|
|
82
|
-
const
|
|
89
|
+
const sp = e && e.schemaPath ? e.schemaPath : '#';
|
|
90
|
+
let k = _keys.get(sp);
|
|
91
|
+
if (k !== undefined) return k;
|
|
92
|
+
const rel = pointerOf(sp);
|
|
83
93
|
const segs = segments(rel);
|
|
84
|
-
|
|
94
|
+
k = { rel, segs, kinds: classify(segs) };
|
|
95
|
+
if (_keys.size >= 4096) _keys.clear();
|
|
96
|
+
_keys.set(sp, k);
|
|
97
|
+
return k;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
|
|
100
|
+
// `rankAt(segs, i)`, when given, can rank a keyword by where it sits: a
|
|
101
|
+
// oneOf the discriminator decides is reported where the discriminator is.
|
|
102
|
+
function sortByKeys(errors, keys, rankAt) {
|
|
88
103
|
if (errors.length < 2) return errors.slice();
|
|
89
104
|
const keyed = errors.map((e) => ({ e, k: keys.get(e) }));
|
|
90
105
|
keyed.sort((A, B) => {
|
|
@@ -93,8 +108,8 @@ function sortByKeys(errors, keys) {
|
|
|
93
108
|
for (let i = 0; i < n; i++) {
|
|
94
109
|
if (pa[i] === pb[i]) continue;
|
|
95
110
|
if (ka[i] !== 'kw') return 0;
|
|
96
|
-
const ra = REFERENCE_RANK.get(pa[i]);
|
|
97
|
-
const rb = REFERENCE_RANK.get(pb[i]);
|
|
111
|
+
const ra = rankAt ? rankAt(pa, i) : REFERENCE_RANK.get(pa[i]);
|
|
112
|
+
const rb = rankAt ? rankAt(pb, i) : REFERENCE_RANK.get(pb[i]);
|
|
98
113
|
if (ra === undefined || rb === undefined) return 0;
|
|
99
114
|
return ra - rb;
|
|
100
115
|
}
|
|
@@ -143,7 +158,22 @@ function asReferenceKeyword(e) {
|
|
|
143
158
|
return e.keyword === 'not' && e.message === 'boolean schema is false' ? { ...e, keyword: 'false schema' } : e;
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
|
|
161
|
+
const FRAGMENT = 'ata-compat:fragment:';
|
|
162
|
+
|
|
163
|
+
// Whether a schema holds a reference anywhere, the reference's test for
|
|
164
|
+
// compiling a `$ref` target as a function of its own instead of inlining it.
|
|
165
|
+
const REF_KEYWORDS = new Set(['$ref', '$recursiveRef', '$recursiveAnchor', '$dynamicRef', '$dynamicAnchor']);
|
|
166
|
+
function hasRef(schema) {
|
|
167
|
+
if (schema === null || typeof schema !== 'object') return false;
|
|
168
|
+
for (const key in schema) {
|
|
169
|
+
if (REF_KEYWORDS.has(key)) return true;
|
|
170
|
+
const sch = schema[key];
|
|
171
|
+
if (Array.isArray(sch) ? sch.some(hasRef) : (sch !== null && typeof sch === 'object' && hasRef(sch))) return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors, macroKeywords, discriminator }) {
|
|
147
177
|
const branchCache = new Map();
|
|
148
178
|
const branchValidator = (pointer) => {
|
|
149
179
|
let v = branchCache.get(pointer);
|
|
@@ -156,6 +186,74 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
156
186
|
const wrappers = new Set(WRAPPERS);
|
|
157
187
|
for (const k of macroKeywords) wrappers.add(k);
|
|
158
188
|
|
|
189
|
+
// Where a schema path really points. ata reports a keyword reached through
|
|
190
|
+
// a local `$ref` at the referring node (`#/properties/s/maxLength`), the
|
|
191
|
+
// reference at the node the `$ref` names (`#/definitions/S/maxLength`).
|
|
192
|
+
// Walking the document, a segment the node does not have, on a node with a
|
|
193
|
+
// `#/...` `$ref`, continues at the target. Anything else stays as it is.
|
|
194
|
+
// Under the discriminator option a oneOf beside a `discriminator` is the
|
|
195
|
+
// discriminator's: its branch errors come where that keyword runs.
|
|
196
|
+
const discriminated = discriminator === true;
|
|
197
|
+
const rankAt = (segs, i) => {
|
|
198
|
+
if (segs[i] === 'oneOf') {
|
|
199
|
+
const node = walkPointer(rootDoc, canonical(segs.length && i > 0 ? '/' + segs.slice(0, i).join('/') : ''));
|
|
200
|
+
if (node !== null && typeof node === 'object' && node.discriminator && typeof node.discriminator.propertyName === 'string') return REFERENCE_RANK.get('discriminator');
|
|
201
|
+
}
|
|
202
|
+
return REFERENCE_RANK.get(segs[i]);
|
|
203
|
+
};
|
|
204
|
+
const canonCache = new Map();
|
|
205
|
+
const reportCache = new Map();
|
|
206
|
+
// `reported` false: the pointer of the node itself, to read the schema at.
|
|
207
|
+
// `reported` true: the path the reference reports, which differs after a
|
|
208
|
+
// `$ref` it compiles as a function of its own rather than inlining (one
|
|
209
|
+
// whose target holds a `$ref`): errors from there carry the path from
|
|
210
|
+
// that target, `#/...`, as if it were a root.
|
|
211
|
+
function follow(pointer, reported) {
|
|
212
|
+
const cache = reported ? reportCache : canonCache;
|
|
213
|
+
let out = cache.get(pointer);
|
|
214
|
+
if (out !== undefined) return out;
|
|
215
|
+
out = pointer;
|
|
216
|
+
let node = rootDoc, path = '', moved = false, ok = true;
|
|
217
|
+
for (const raw of segments(pointer)) {
|
|
218
|
+
const seg = unescape(raw);
|
|
219
|
+
let first = null, hops = 0;
|
|
220
|
+
for (; hops < 32 && node !== null && typeof node === 'object' && !Array.isArray(node) &&
|
|
221
|
+
!Object.prototype.hasOwnProperty.call(node, seg) && typeof node.$ref === 'string' &&
|
|
222
|
+
(node.$ref.startsWith('#/') || node.$ref.startsWith(FRAGMENT)); hops++) {
|
|
223
|
+
// A schema registered under a fragment-only key (see fragmentKey in
|
|
224
|
+
// compat.js) is named by that key, as the reference names it.
|
|
225
|
+
const registered = node.$ref.startsWith(FRAGMENT);
|
|
226
|
+
if (registered && !reported) { ok = false; break; }
|
|
227
|
+
let target;
|
|
228
|
+
try { target = decodeURIComponent(registered ? node.$ref.slice(FRAGMENT.length) : node.$ref.slice(1)); } catch { target = null; }
|
|
229
|
+
if (target === null) { ok = false; break; }
|
|
230
|
+
node = registered ? (schemas ? schemas[node.$ref] : undefined) : walkPointer(rootDoc, target);
|
|
231
|
+
if (first === null) first = target;
|
|
232
|
+
if (!reported) path = target;
|
|
233
|
+
moved = true;
|
|
234
|
+
}
|
|
235
|
+
if (!ok) break;
|
|
236
|
+
// A `$ref` to a `$ref` is reported at the first target, as the
|
|
237
|
+
// reference reports it, unless the reference compiled the last one on
|
|
238
|
+
// its own.
|
|
239
|
+
if (reported && first !== null) path = hasRef(node) ? '' : first;
|
|
240
|
+
if (node === null || typeof node !== 'object') { ok = false; break; }
|
|
241
|
+
node = Array.isArray(node) ? node[Number(seg)] : node[seg];
|
|
242
|
+
path += '/' + raw;
|
|
243
|
+
}
|
|
244
|
+
if (ok && moved && node !== undefined) out = path;
|
|
245
|
+
if (cache.size >= 4096) cache.clear();
|
|
246
|
+
cache.set(pointer, out);
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
const canonical = (pointer) => follow(pointer, false);
|
|
250
|
+
function canonicalError(e) {
|
|
251
|
+
const sp = e.schemaPath;
|
|
252
|
+
if (typeof sp !== 'string' || !sp.startsWith('#')) return e;
|
|
253
|
+
const c = follow(sp.slice(1), true);
|
|
254
|
+
return c === sp.slice(1) ? e : { ...e, schemaPath: '#' + c };
|
|
255
|
+
}
|
|
256
|
+
|
|
159
257
|
// Errors whose schemaPath is already absolute (from the root), as opposed
|
|
160
258
|
// to relative to the branch validator that produced them.
|
|
161
259
|
const absolute = new WeakSet();
|
|
@@ -163,14 +261,16 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
163
261
|
// Returns the shaped errors of `value` against the subschema at `pointer`
|
|
164
262
|
// (absolute), with schemaPath made absolute and instancePath rebased onto
|
|
165
263
|
// the caller's.
|
|
166
|
-
|
|
264
|
+
// `pointer` is where the subschema is; `at`, when given, is the path its
|
|
265
|
+
// errors are reported under, the referring one ata's own errors use.
|
|
266
|
+
function subErrors(pointer, value, schemaPrefix, instancePrefix, at) {
|
|
167
267
|
const r = branchValidator(pointer).validate(value);
|
|
168
268
|
if (r.valid) return null;
|
|
169
269
|
const shaped = shape(r.errors, value, pointer);
|
|
170
270
|
return shaped.map((e) => {
|
|
171
271
|
const out = {
|
|
172
272
|
...e,
|
|
173
|
-
schemaPath: absolute.has(e) ? e.schemaPath : schemaPrefix + pointer + pointerOf(e.schemaPath || '#'),
|
|
273
|
+
schemaPath: absolute.has(e) ? e.schemaPath : schemaPrefix + (at === undefined ? pointer : at) + pointerOf(e.schemaPath || '#'),
|
|
174
274
|
instancePath: instancePrefix + (e.instancePath || ''),
|
|
175
275
|
};
|
|
176
276
|
absolute.add(out);
|
|
@@ -187,7 +287,7 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
187
287
|
function shape(errors, data, basePointer) {
|
|
188
288
|
const keys = new Map();
|
|
189
289
|
for (const e of errors) keys.set(e, keyFor(e));
|
|
190
|
-
const sorted = sortByKeys(errors, keys);
|
|
290
|
+
const sorted = sortByKeys(errors, keys, discriminated ? rankAt : undefined);
|
|
191
291
|
const out = [];
|
|
192
292
|
const consumed = new Set();
|
|
193
293
|
let thenGroup = null; // { prefix, which } for the pending `if` wrapper
|
|
@@ -208,7 +308,8 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
208
308
|
if (consumed.has(e)) continue;
|
|
209
309
|
const K = keys.get(e);
|
|
210
310
|
const rel = K.rel;
|
|
211
|
-
const
|
|
311
|
+
const raw = basePointer + rel;
|
|
312
|
+
const abs = canonical(raw);
|
|
212
313
|
const segs = K.segs;
|
|
213
314
|
const kinds = K.kinds;
|
|
214
315
|
const schemaPrefix = (e.schemaPath || '#').slice(0, (e.schemaPath || '#').indexOf('#') + 1);
|
|
@@ -241,14 +342,25 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
241
342
|
const value = walkData(data, e.instancePath || '');
|
|
242
343
|
const branchErrors = [];
|
|
243
344
|
const passing = [];
|
|
345
|
+
// The validator kept every branch's errors when keywords may have
|
|
346
|
+
// changed the data: validating a branch again would see the change.
|
|
347
|
+
const kept = basePointer === '' && Array.isArray(e._branches) && e._branches.length === branches.length ? e._branches : null;
|
|
244
348
|
for (let i = 0; i < branches.length; i++) {
|
|
245
|
-
|
|
349
|
+
let errs;
|
|
350
|
+
if (kept !== null) {
|
|
351
|
+
errs = kept[i].valid ? null : shape(kept[i].errors, data, '');
|
|
352
|
+
if (errs !== null) for (const x of errs) absolute.add(x);
|
|
353
|
+
} else errs = subErrors(abs + '/' + i, value, schemaPrefix, e.instancePath || '', raw + '/' + i);
|
|
246
354
|
if (errs === null) passing.push(i);
|
|
247
355
|
else branchErrors.push(allErrors ? errs : errs.slice(0, groupEnd(errs)));
|
|
248
356
|
}
|
|
249
357
|
if (passing.length === 0) {
|
|
250
358
|
for (const errs of branchErrors) out.push(...errs);
|
|
251
|
-
|
|
359
|
+
// The reference's own wording and params for no branch passing;
|
|
360
|
+
// the collapsed error validate() reports names the count instead.
|
|
361
|
+
out.push(kw === 'oneOf'
|
|
362
|
+
? keywordError('oneOf', e.instancePath || '', e.schemaPath, { passingSchemas: null }, 'must match exactly one schema in oneOf')
|
|
363
|
+
: keywordError('anyOf', e.instancePath || '', e.schemaPath, {}, 'must match a schema in anyOf'));
|
|
252
364
|
} else if (kw === 'oneOf') {
|
|
253
365
|
out.push({ ...e, params: { passingSchemas: passing }, message: 'must match exactly one schema in oneOf' });
|
|
254
366
|
} else {
|
|
@@ -291,7 +403,7 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
291
403
|
const items = walkData(data, e.instancePath || '');
|
|
292
404
|
if (Array.isArray(items)) {
|
|
293
405
|
for (let i = 0; i < items.length; i++) {
|
|
294
|
-
const errs = subErrors(abs, items[i], schemaPrefix, (e.instancePath || '') + '/' + i);
|
|
406
|
+
const errs = subErrors(abs, items[i], schemaPrefix, (e.instancePath || '') + '/' + i, raw);
|
|
295
407
|
if (errs !== null) out.push(...errs);
|
|
296
408
|
}
|
|
297
409
|
out.push(e);
|
|
@@ -304,7 +416,7 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
304
416
|
const depIndex = segs.findIndex((s, i) => kinds[i] === 'kw' && (s === 'dependentRequired' || s === 'dependentSchemas'));
|
|
305
417
|
if (depIndex >= 0) {
|
|
306
418
|
const nodePointer = basePointer + (depIndex === 0 ? '' : '/' + segs.slice(0, depIndex).join('/'));
|
|
307
|
-
const node = walkPointer(rootDoc, nodePointer);
|
|
419
|
+
const node = walkPointer(rootDoc, canonical(nodePointer));
|
|
308
420
|
if (node && typeof node === 'object' && node.dependencies && typeof node.dependencies === 'object') {
|
|
309
421
|
const rewritten = segs.slice();
|
|
310
422
|
rewritten[depIndex] = 'dependencies';
|
|
@@ -379,7 +491,7 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
379
491
|
if (e.keyword !== 'type' || typeof e.message !== 'string' || !e.message.startsWith('must be ')) return e;
|
|
380
492
|
const ptr = pointerOf(e.schemaPath || '');
|
|
381
493
|
if (!ptr.endsWith('/type')) return e;
|
|
382
|
-
const node = walkPointer(rootDoc, ptr.slice(0, -'/type'.length));
|
|
494
|
+
const node = walkPointer(rootDoc, canonical(ptr.slice(0, -'/type'.length)));
|
|
383
495
|
let t = node && typeof node === 'object' && node.type !== undefined ? node.type : e.params && e.params.type;
|
|
384
496
|
if (t === undefined) return e;
|
|
385
497
|
if (node && node.nullable === true && Array.isArray(t) && !t.includes('null')) t = [...t, 'null'];
|
|
@@ -387,10 +499,12 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
387
499
|
}
|
|
388
500
|
|
|
389
501
|
return function shapeErrors(errors, data) {
|
|
502
|
+
// Paths are made the reference's last: the grouping above reads them as
|
|
503
|
+
// ata reports them, through the referring node.
|
|
390
504
|
const shaped = shape(errors, data, '').map(asReferenceKeyword).map(asReferenceType);
|
|
391
|
-
if (allErrors) return shaped;
|
|
505
|
+
if (allErrors) return shaped.map(canonicalError);
|
|
392
506
|
const n = groupEnd(shaped);
|
|
393
|
-
return shaped.slice(0, n);
|
|
507
|
+
return shaped.slice(0, n).map(canonicalError);
|
|
394
508
|
};
|
|
395
509
|
}
|
|
396
510
|
|
package/lib/draft7.js
CHANGED
|
@@ -91,34 +91,38 @@ function _normalize(schema, d6) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// Recurse into object-valued sub-schemas
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
for (const
|
|
94
|
+
for (const key of D7_OBJ_SUBS) {
|
|
95
|
+
const m = schema[key]
|
|
96
|
+
if (m && typeof m === 'object') {
|
|
97
|
+
for (const k in m) {
|
|
98
|
+
if (!Object.prototype.hasOwnProperty.call(m, k)) continue
|
|
99
|
+
const v = m[k]
|
|
98
100
|
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
// Recurse into array-valued sub-schemas
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
if (Array.isArray(
|
|
107
|
-
for (
|
|
108
|
-
|
|
106
|
+
for (const key of D7_ARR_SUBS) {
|
|
107
|
+
const a = schema[key]
|
|
108
|
+
if (Array.isArray(a)) {
|
|
109
|
+
for (let i = 0; i < a.length; i++) {
|
|
110
|
+
const v = a[i]
|
|
111
|
+
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
// Recurse into single sub-schemas
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
118
|
-
_normalize(schema[key], d6)
|
|
119
|
-
}
|
|
117
|
+
for (const key of D7_SINGLE_SUBS) {
|
|
118
|
+
const v = schema[key]
|
|
119
|
+
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
// The schema-bearing keywords _normalize walks, built once rather than per node.
|
|
123
|
+
const D7_OBJ_SUBS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
124
|
+
const D7_ARR_SUBS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
|
|
125
|
+
const D7_SINGLE_SUBS = ['items', 'contains', 'not', 'if', 'then', 'else', 'additionalProperties', 'propertyNames']
|
|
122
126
|
|
|
123
127
|
// OpenAPI `nullable: true` is not JSON Schema. Convert it to a union with
|
|
124
128
|
// 'null' (`{ type: 'X', nullable: true }` -> `{ type: ['X', 'null'] }`), the
|
|
@@ -170,12 +174,36 @@ function _walkExclusive(node, seen) {
|
|
|
170
174
|
|
|
171
175
|
function normalizeNullable(schema) {
|
|
172
176
|
if (typeof schema !== 'object' || schema === null) return schema
|
|
173
|
-
|
|
177
|
+
const st = { seen: new Set(), refs: [] }
|
|
178
|
+
_normalizeNullable(schema, st)
|
|
179
|
+
// A local `$ref` can point anywhere in the document, an OpenAPI
|
|
180
|
+
// `components` section among them, where the walk over subschema keywords
|
|
181
|
+
// does not go. A schema reached that way is a schema all the same, and its
|
|
182
|
+
// `nullable` is folded too.
|
|
183
|
+
while (st.refs.length > 0) {
|
|
184
|
+
const target = _localTarget(schema, st.refs.pop())
|
|
185
|
+
if (typeof target === 'object' && target !== null && !Array.isArray(target)) _normalizeNullable(target, st)
|
|
186
|
+
}
|
|
174
187
|
return schema
|
|
175
188
|
}
|
|
176
189
|
|
|
177
|
-
function
|
|
190
|
+
function _localTarget(root, ref) {
|
|
191
|
+
if (ref === '#') return root
|
|
192
|
+
if (!ref.startsWith('#/')) return undefined
|
|
193
|
+
let node = root
|
|
194
|
+
for (const raw of ref.slice(2).split('/')) {
|
|
195
|
+
let seg = raw.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
196
|
+
if (seg.includes('%')) { try { seg = decodeURIComponent(seg) } catch { return undefined } }
|
|
197
|
+
if (node === null || typeof node !== 'object' || !Object.prototype.hasOwnProperty.call(node, seg)) return undefined
|
|
198
|
+
node = node[seg]
|
|
199
|
+
}
|
|
200
|
+
return node
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function _normalizeNullable(schema, st) {
|
|
178
204
|
if (typeof schema !== 'object' || schema === null) return
|
|
205
|
+
if (st.seen.has(schema)) return
|
|
206
|
+
st.seen.add(schema)
|
|
179
207
|
|
|
180
208
|
if (schema.nullable === true && schema.type !== undefined) {
|
|
181
209
|
if (Array.isArray(schema.type)) {
|
|
@@ -195,37 +223,39 @@ function _normalizeNullable(schema) {
|
|
|
195
223
|
// alone: the engines decode once more, and `100%` must not be decoded twice.
|
|
196
224
|
// This walk is the one every normalization runs, so it costs no second pass;
|
|
197
225
|
// a reference it does not reach keeps the old behaviour, the interpreter.
|
|
198
|
-
if (
|
|
226
|
+
if (typeof schema.$ref === 'string' && schema.$ref.charCodeAt(0) === 35 && schema.$ref.indexOf('%') > 0) {
|
|
199
227
|
try {
|
|
200
228
|
const decoded = decodeURIComponent(schema.$ref)
|
|
201
229
|
if (!decoded.includes('%')) schema.$ref = decoded
|
|
202
230
|
} catch {}
|
|
203
231
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const key of
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
232
|
+
if (typeof schema.$ref === 'string' && schema.$ref.charCodeAt(0) === 35) st.refs.push(schema.$ref)
|
|
233
|
+
|
|
234
|
+
for (const key of D7_OBJ_SUBS) {
|
|
235
|
+
const m = schema[key]
|
|
236
|
+
if (m && typeof m === 'object') {
|
|
237
|
+
for (const k in m) {
|
|
238
|
+
if (!Object.prototype.hasOwnProperty.call(m, k)) continue
|
|
239
|
+
const v = m[k]
|
|
240
|
+
if (typeof v === 'object' && v !== null) _normalizeNullable(v, st)
|
|
210
241
|
}
|
|
211
242
|
}
|
|
212
243
|
}
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
if (Array.isArray(
|
|
216
|
-
for (
|
|
217
|
-
|
|
244
|
+
for (const key of D7_ARR_SUBS) {
|
|
245
|
+
const a = schema[key]
|
|
246
|
+
if (Array.isArray(a)) {
|
|
247
|
+
for (let i = 0; i < a.length; i++) {
|
|
248
|
+
const v = a[i]
|
|
249
|
+
if (typeof v === 'object' && v !== null) _normalizeNullable(v, st)
|
|
218
250
|
}
|
|
219
251
|
}
|
|
220
252
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
225
|
-
_normalizeNullable(schema[key])
|
|
226
|
-
}
|
|
253
|
+
for (const key of NULLABLE_SINGLE_SUBS) {
|
|
254
|
+
const v = schema[key]
|
|
255
|
+
if (typeof v === 'object' && v !== null) _normalizeNullable(v, st)
|
|
227
256
|
}
|
|
228
257
|
}
|
|
258
|
+
const NULLABLE_SINGLE_SUBS = ['items', 'contains', 'not', 'if', 'then', 'else', 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties']
|
|
229
259
|
|
|
230
260
|
// Draft 2020-12 treats `format` as an annotation unless the assertion
|
|
231
261
|
// vocabulary is in use. Removing the keyword is exactly that reading, and it
|
package/lib/error-runtime.js
CHANGED
|
@@ -94,7 +94,11 @@ function errorRuntimeSource(format, version, features, schemaMode) {
|
|
|
94
94
|
const src = OPTIONAL[name] !== undefined && !want(name)
|
|
95
95
|
? OPTIONAL[name].stub
|
|
96
96
|
: dropCommentLines(fs.readFileSync(path.join(__dirname, name + '.js'), 'utf8'), name);
|
|
97
|
-
|
|
97
|
+
// The files' own require('./x') calls go to the table under another name:
|
|
98
|
+
// a tool that reads the runtime without scope (knip, a lint rule) took a
|
|
99
|
+
// local `require` for an import of a file that is not there.
|
|
100
|
+
const own = src.replace(/\brequire\((['"])\.\//g, '__ataReq($1./');
|
|
101
|
+
defs.push(`${JSON.stringify(name)}: function (module, exports, __ataReq) {\n${own}\n}`);
|
|
98
102
|
}
|
|
99
103
|
const { byKw, byFmt } = codeTable();
|
|
100
104
|
const body = `// Auto-generated by ata-validator ${version}, do not edit.
|