ata-validator 1.52.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/compat.js +314 -29
- package/lib/compat-errors.js +131 -17
- package/lib/draft7.js +65 -35
- package/lib/interpreter-core.js +297 -14
- package/lib/js-compiler.js +3 -1
- package/lib/keywords.js +24 -1
- package/lib/levenshtein.js +21 -5
- package/lib/plan-compiler.js +8 -3
- package/lib/schema-scan.js +29 -1
- package/lib/validator-core.js +99 -13
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/lib/plan-compiler.js
CHANGED
|
@@ -532,8 +532,11 @@ function install(deps) {
|
|
|
532
532
|
const key = keys[k];
|
|
533
533
|
let evaluated = false;
|
|
534
534
|
const prop = lookup !== null ? lookup(key) : undefined;
|
|
535
|
+
// A declared property whose value is undefined is not checked (see
|
|
536
|
+
// the interpreter's eval).
|
|
535
537
|
if (prop !== undefined) {
|
|
536
|
-
|
|
538
|
+
const value = data[key];
|
|
539
|
+
if (value !== undefined && !((prop.fn.leaf && prop.fn.v(value, stack)) || prop.fn.c(value, errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack))) {
|
|
537
540
|
ok = false;
|
|
538
541
|
if (errors === NOERRORS) return false;
|
|
539
542
|
}
|
|
@@ -578,7 +581,8 @@ function install(deps) {
|
|
|
578
581
|
for (let i = 0; i < n; i++) {
|
|
579
582
|
const key = propKeys[i];
|
|
580
583
|
if (!Object.hasOwn(data, key)) continue;
|
|
581
|
-
|
|
584
|
+
const value = data[key];
|
|
585
|
+
if (value !== undefined && !propList[i].fn.v(value, stack)) return false;
|
|
582
586
|
if (collect) addProp(rec, key);
|
|
583
587
|
}
|
|
584
588
|
return true;
|
|
@@ -594,7 +598,8 @@ function install(deps) {
|
|
|
594
598
|
let evaluated = false;
|
|
595
599
|
const prop = lookup !== null ? lookup(key) : undefined;
|
|
596
600
|
if (prop !== undefined) {
|
|
597
|
-
|
|
601
|
+
const value = data[key];
|
|
602
|
+
if (value !== undefined && !prop.fn.v(value, stack)) return false;
|
|
598
603
|
evaluated = true;
|
|
599
604
|
}
|
|
600
605
|
if (patterns !== null) {
|
package/lib/schema-scan.js
CHANGED
|
@@ -30,6 +30,24 @@ const TUPLE_ITEMS = 1 << 5; // array-valued `items`, split into `prefixItem
|
|
|
30
30
|
const EXCLUSIVE_BOOL = 1 << 6; // draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum`
|
|
31
31
|
const ENCODED_REF = 1 << 7; // a fragment `$ref` holding a percent escape, decoded by normalizeNullable's walk
|
|
32
32
|
|
|
33
|
+
// Questions compat asks of a schema it is about to compile (compat.js), read
|
|
34
|
+
// on the same walk so that a document is not walked again for them.
|
|
35
|
+
const HAS_REF = 1 << 8; // `$ref` anywhere
|
|
36
|
+
const BEYOND_ITEMS = 1 << 9; // a subschema keyword other than `properties` and object `items`
|
|
37
|
+
const DISCRIMINATOR = 1 << 10; // `discriminator` anywhere
|
|
38
|
+
const HAS_DEFAULT = 1 << 11; // `default` anywhere
|
|
39
|
+
const HAS_ALLOF = 1 << 12; // `allOf` anywhere
|
|
40
|
+
const HAS_FORMAT = 1 << 13; // `format` anywhere
|
|
41
|
+
const KEY_BITS = new Map([
|
|
42
|
+
['$ref', HAS_REF | BEYOND_ITEMS], ['$dynamicRef', BEYOND_ITEMS], ['allOf', HAS_ALLOF | BEYOND_ITEMS],
|
|
43
|
+
['anyOf', BEYOND_ITEMS], ['oneOf', BEYOND_ITEMS], ['not', BEYOND_ITEMS], ['if', BEYOND_ITEMS],
|
|
44
|
+
['then', BEYOND_ITEMS], ['else', BEYOND_ITEMS], ['additionalProperties', BEYOND_ITEMS],
|
|
45
|
+
['patternProperties', BEYOND_ITEMS], ['prefixItems', BEYOND_ITEMS], ['additionalItems', BEYOND_ITEMS],
|
|
46
|
+
['contains', BEYOND_ITEMS], ['dependentSchemas', BEYOND_ITEMS], ['dependencies', BEYOND_ITEMS],
|
|
47
|
+
['propertyNames', BEYOND_ITEMS], ['unevaluatedProperties', BEYOND_ITEMS], ['unevaluatedItems', BEYOND_ITEMS],
|
|
48
|
+
['discriminator', DISCRIMINATOR], ['default', HAS_DEFAULT], ['format', HAS_FORMAT],
|
|
49
|
+
]);
|
|
50
|
+
|
|
33
51
|
// Everything only draft-07 normalization acts on. A schema of another dialect
|
|
34
52
|
// can carry these without them meaning anything, so they are read together
|
|
35
53
|
// with whether the document is draft-07 at all.
|
|
@@ -92,7 +110,11 @@ function scan(schema) {
|
|
|
92
110
|
}
|
|
93
111
|
// Every key, not just the subschema keywords, so this cannot miss a place
|
|
94
112
|
// the normalizers would reach.
|
|
95
|
-
for (let i = 0; i < keys.length; i++)
|
|
113
|
+
for (let i = 0; i < keys.length; i++) {
|
|
114
|
+
const b = KEY_BITS.get(keys[i]);
|
|
115
|
+
if (b !== undefined) bits |= b;
|
|
116
|
+
walk(node[keys[i]]);
|
|
117
|
+
}
|
|
96
118
|
};
|
|
97
119
|
|
|
98
120
|
walk(schema);
|
|
@@ -124,4 +146,10 @@ module.exports = {
|
|
|
124
146
|
EXCLUSIVE_BOOL,
|
|
125
147
|
ENCODED_REF,
|
|
126
148
|
DRAFT7_WORK,
|
|
149
|
+
HAS_REF,
|
|
150
|
+
BEYOND_ITEMS,
|
|
151
|
+
DISCRIMINATOR,
|
|
152
|
+
HAS_DEFAULT,
|
|
153
|
+
HAS_ALLOF,
|
|
154
|
+
HAS_FORMAT,
|
|
127
155
|
};
|
package/lib/validator-core.js
CHANGED
|
@@ -424,11 +424,22 @@ function _deepCloneWithSymbols(v) {
|
|
|
424
424
|
return a;
|
|
425
425
|
}
|
|
426
426
|
// Only clone plain objects (skip RegExp, Date, etc.).
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
427
|
+
const proto = Object.getPrototypeOf(v);
|
|
428
|
+
if (proto !== Object.prototype && proto !== null) return v;
|
|
429
|
+
// An object literal, filled by assignment: building it without a prototype
|
|
430
|
+
// and setting one at the end left every copy in V8's slow dictionary mode,
|
|
431
|
+
// which made the copy and every walk over it slower. Only a key named
|
|
432
|
+
// `__proto__` needs defineProperty to stay an own property.
|
|
433
|
+
const out = {};
|
|
434
|
+
const keys = Object.keys(v);
|
|
435
|
+
for (let i = 0; i < keys.length; i++) {
|
|
436
|
+
const k = keys[i];
|
|
437
|
+
if (k === '__proto__') Object.defineProperty(out, k, { value: _deepCloneWithSymbols(v[k]), writable: true, enumerable: true, configurable: true });
|
|
438
|
+
else out[k] = _deepCloneWithSymbols(v[k]);
|
|
439
|
+
}
|
|
440
|
+
const syms = Object.getOwnPropertySymbols(v);
|
|
441
|
+
for (let i = 0; i < syms.length; i++) out[syms[i]] = v[syms[i]];
|
|
442
|
+
return out;
|
|
432
443
|
}
|
|
433
444
|
|
|
434
445
|
// Normalize a caller-provided schema without mutating the original.
|
|
@@ -436,7 +447,27 @@ function _deepCloneWithSymbols(v) {
|
|
|
436
447
|
// present or nullable fields present). Internal-only — not exported.
|
|
437
448
|
// `inheritDraft7` is the root's draft for a document that declares none of
|
|
438
449
|
// its own: 0 or absent for none, 1 for draft-07, 2 for draft-06, 3 for 2019-09.
|
|
450
|
+
// Normalized results per schema object and dialect. A registry of hundreds of
|
|
451
|
+
// schemas (an OpenAPI document's components, registered one by one) is
|
|
452
|
+
// normalized again for every validator built over it when the map holding
|
|
453
|
+
// them is a new object each time; the schemas themselves are the same objects,
|
|
454
|
+
// and a schema is not changed after it is handed to a validator.
|
|
455
|
+
const _normalized = new WeakMap()
|
|
439
456
|
function _normalizeCallerSchema(s, inheritDraft7) {
|
|
457
|
+
if (s === null || typeof s !== 'object') return _normalizeCallerSchemaUncached(s, inheritDraft7)
|
|
458
|
+
const slot = inheritDraft7 || 0
|
|
459
|
+
let byDraft = _normalized.get(s)
|
|
460
|
+
if (byDraft !== undefined && byDraft[slot] !== undefined) return byDraft[slot]
|
|
461
|
+
const out = _normalizeCallerSchemaUncached(s, inheritDraft7)
|
|
462
|
+
// Only a copy is kept: a schema that needs nothing is answered by the scan
|
|
463
|
+
// alone, and remembering it cost every such validator a slot.
|
|
464
|
+
if (out !== s) {
|
|
465
|
+
if (byDraft === undefined) { byDraft = [undefined, undefined, undefined, undefined]; _normalized.set(s, byDraft) }
|
|
466
|
+
byDraft[slot] = out
|
|
467
|
+
}
|
|
468
|
+
return out
|
|
469
|
+
}
|
|
470
|
+
function _normalizeCallerSchemaUncached(s, inheritDraft7) {
|
|
440
471
|
const declared = s && typeof s === 'object' ? s.$schema : undefined
|
|
441
472
|
const code = declared !== undefined ? draftCode(declared) : (inheritDraft7 || 0)
|
|
442
473
|
const needsDraft7 = code === 1 || code === 2
|
|
@@ -450,7 +481,6 @@ function _normalizeCallerSchema(s, inheritDraft7) {
|
|
|
450
481
|
// The scan does not look for `if`/`then`/`else`, which draft-06 drops.
|
|
451
482
|
if (!draft6 && !draft2019 && !needsNormalization(s, needsDraft7)) return s
|
|
452
483
|
|
|
453
|
-
const str = JSON.stringify(s)
|
|
454
484
|
const copy = _deepCloneWithSymbols(s)
|
|
455
485
|
if (needsDraft7) normalizeDraft7(copy, true, draft6)
|
|
456
486
|
if (draft2019) normalizeDraft2019(copy, true)
|
|
@@ -459,8 +489,49 @@ function _normalizeCallerSchema(s, inheritDraft7) {
|
|
|
459
489
|
// Return original when normalization produced no change, copy otherwise.
|
|
460
490
|
// Kept even though the walk has already said there is work, so that a walk
|
|
461
491
|
// which over-reports still returns exactly what it returned before.
|
|
462
|
-
// Change-detection
|
|
463
|
-
|
|
492
|
+
// Change-detection is JSON content, key order included; symbols do not
|
|
493
|
+
// affect it. Compared in place: serializing both took two passes over the
|
|
494
|
+
// whole document on every compile, most of the compile time of an OpenAPI
|
|
495
|
+
// document that inlines its components into each operation's schema, and
|
|
496
|
+
// a change is usually found in the first few nodes.
|
|
497
|
+
return _jsonEqual(s, copy) ? s : copy
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Whether `a` and `b` serialize to the same JSON, without serializing: the
|
|
501
|
+
// copy is a clone of `a`, so anything the clone shares by reference (a
|
|
502
|
+
// function, a RegExp) is the same on both sides.
|
|
503
|
+
function _jsonEqual (a, b) {
|
|
504
|
+
if (a === b) return true
|
|
505
|
+
if (typeof a === 'number' && typeof b === 'number') return a !== a && b !== b
|
|
506
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
|
|
507
|
+
const arr = Array.isArray(a)
|
|
508
|
+
if (arr !== Array.isArray(b)) return false
|
|
509
|
+
if (arr) {
|
|
510
|
+
if (a.length !== b.length) return false
|
|
511
|
+
for (let i = 0; i < a.length; i++) if (!_jsonEqual(a[i], b[i])) return false
|
|
512
|
+
return true
|
|
513
|
+
}
|
|
514
|
+
const ka = _jsonKeys(a), kb = _jsonKeys(b)
|
|
515
|
+
if (ka.length !== kb.length) return false
|
|
516
|
+
for (let i = 0; i < ka.length; i++) {
|
|
517
|
+
if (ka[i] !== kb[i]) return false
|
|
518
|
+
if (!_jsonEqual(a[ka[i]], b[kb[i]])) return false
|
|
519
|
+
}
|
|
520
|
+
return true
|
|
521
|
+
}
|
|
522
|
+
// The keys JSON.stringify writes: own enumerable ones whose value is not
|
|
523
|
+
// undefined, a function or a symbol.
|
|
524
|
+
function _jsonKeys (o) {
|
|
525
|
+
const keys = Object.keys(o)
|
|
526
|
+
let out = null
|
|
527
|
+
for (let i = 0; i < keys.length; i++) {
|
|
528
|
+
const v = o[keys[i]]
|
|
529
|
+
const t = typeof v
|
|
530
|
+
if (v === undefined || t === 'function' || t === 'symbol') {
|
|
531
|
+
if (out === null) out = keys.slice(0, i)
|
|
532
|
+
} else if (out !== null) out.push(keys[i])
|
|
533
|
+
}
|
|
534
|
+
return out === null ? keys : out
|
|
464
535
|
}
|
|
465
536
|
|
|
466
537
|
// The identity a document is registered under. Draft-07 ignores every
|
|
@@ -650,7 +721,10 @@ function _materializeSchema(self) {
|
|
|
650
721
|
}
|
|
651
722
|
const isCallers = self._rawIsCallers && schemaObj === raw;
|
|
652
723
|
if (options.assertFormat === false) {
|
|
653
|
-
|
|
724
|
+
// Always on a copy: a normalized schema is shared by every validator built
|
|
725
|
+
// from the same object (see _normalizeCallerSchema), so stripping in place
|
|
726
|
+
// would take format checks away from the others.
|
|
727
|
+
schemaObj = stripFormatAssertions(_deepCloneWithSymbols(schemaObj));
|
|
654
728
|
}
|
|
655
729
|
// A keyword used only in a schema reached through $ref counts too: the
|
|
656
730
|
// interpreted engine applies it there, and every path that does not know the
|
|
@@ -952,7 +1026,10 @@ class Validator {
|
|
|
952
1026
|
// declines the schema, so does everything else, and the interpreted
|
|
953
1027
|
// engine answers alone, as it always did.
|
|
954
1028
|
const kw = this._keywords ? { keywords: this._keywords } : undefined;
|
|
955
|
-
|
|
1029
|
+
// A keyword that reads its data's context is only answered by the
|
|
1030
|
+
// interpreted engine's collecting walk (see lib/keywords.js).
|
|
1031
|
+
const ctxKw = this._usesKeywords && Object.keys(this._keywords).some((k) => this._keywords[k].dataContext);
|
|
1032
|
+
const _cgFn = ctxKw ? null : compileToJSCodegen(schemaObj, sm, uf, kw);
|
|
956
1033
|
jsFn = _cgFn || (this._usesKeywords ? null : compileToJS(schemaObj, null, sm));
|
|
957
1034
|
// Only the verdict is compiled here. The error and combined generators
|
|
958
1035
|
// are the other two thirds of a cold first call (8.2, 10.4 and 7.8 ms on
|
|
@@ -1119,9 +1196,18 @@ class Validator {
|
|
|
1119
1196
|
formats: this._userFormats,
|
|
1120
1197
|
v1: isV1Dialect(schemaObj),
|
|
1121
1198
|
keywords: this._keywords,
|
|
1199
|
+
// compat's coercion where each type is checked (see the interpreter).
|
|
1200
|
+
coerce: this._interpretOnly ? options._coerceInWalk : undefined,
|
|
1201
|
+
discriminator: this._interpretOnly && options._discriminator === true,
|
|
1202
|
+
numberFormats: this._interpretOnly ? options._numberFormats : undefined,
|
|
1122
1203
|
});
|
|
1123
1204
|
this._engine = 'interpreter';
|
|
1124
|
-
|
|
1205
|
+
// A keyword that reads its data's context may also change the data (a
|
|
1206
|
+
// request validator deserializing a date, say). Such a schema is run
|
|
1207
|
+
// once per call: deciding first and walking again for the errors would
|
|
1208
|
+
// run the keyword twice, the second time on what the first left.
|
|
1209
|
+
const once = interp.dataContext === true;
|
|
1210
|
+
if (!preprocess && !once) this._fastVerdict = (d) => interp.isValid(d);
|
|
1125
1211
|
// abortEarly is a documented contract, not a property of whichever engine
|
|
1126
1212
|
// answered: it promises the frozen ATA9000 stub instead of a detailed
|
|
1127
1213
|
// error, so code that branches on it has to behave the same with and
|
|
@@ -1136,7 +1222,7 @@ class Validator {
|
|
|
1136
1222
|
: (data) => interp.validate(data));
|
|
1137
1223
|
this.validate = this._verbose ? _verboseWrap(run, schemaObj) : run;
|
|
1138
1224
|
// Reading errors runs the interpreter once, directly; see _rejectBase.
|
|
1139
|
-
if (!preprocess && !options.abortEarly && !this._verbose) _rejectBase = (data) => interp.validate(data);
|
|
1225
|
+
if (!preprocess && !options.abortEarly && !this._verbose && !once) _rejectBase = (data) => interp.validate(data);
|
|
1140
1226
|
_bindVerdict(this, this._fastVerdict
|
|
1141
1227
|
? this._fastVerdict
|
|
1142
1228
|
: (data) => run(data).valid);
|
|
@@ -1146,7 +1232,7 @@ class Validator {
|
|
|
1146
1232
|
// that the text path wraps around it: going through validate() as it
|
|
1147
1233
|
// stands once every layer is in place handed the text path errors
|
|
1148
1234
|
// already presented, and they lost their source anchor.
|
|
1149
|
-
const runText = options.abortEarly
|
|
1235
|
+
const runText = options.abortEarly || once
|
|
1150
1236
|
? run
|
|
1151
1237
|
: (preprocess
|
|
1152
1238
|
? (data) => { preprocess(data); return interp.isValid(data) ? VALID_RESULT : interp.validate(data); }
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.0",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -174,13 +174,13 @@
|
|
|
174
174
|
"LICENSE"
|
|
175
175
|
],
|
|
176
176
|
"optionalDependencies": {
|
|
177
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
178
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
179
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
180
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
181
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
182
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
183
|
-
"@ata-validator/native-win32-x64": "1.
|
|
177
|
+
"@ata-validator/native-darwin-arm64": "1.53.0",
|
|
178
|
+
"@ata-validator/native-darwin-x64": "1.53.0",
|
|
179
|
+
"@ata-validator/native-linux-arm64-gnu": "1.53.0",
|
|
180
|
+
"@ata-validator/native-linux-arm64-musl": "1.53.0",
|
|
181
|
+
"@ata-validator/native-linux-x64-gnu": "1.53.0",
|
|
182
|
+
"@ata-validator/native-linux-x64-musl": "1.53.0",
|
|
183
|
+
"@ata-validator/native-win32-x64": "1.53.0"
|
|
184
184
|
},
|
|
185
185
|
"peerDependencies": {
|
|
186
186
|
"yaml": "^2.0.0"
|