ata-validator 1.52.0 → 1.54.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 +7 -0
- package/compat.js +325 -29
- package/lib/codegen-engine.js +2 -1
- package/lib/compat-errors.js +131 -17
- package/lib/dialect.js +36 -1
- package/lib/draft7.js +80 -61
- package/lib/enrich-site.js +29 -2
- package/lib/error-runtime.js +6 -2
- package/lib/formats-source.js +43 -66
- package/lib/formats.js +37 -231
- package/lib/interpreter-compat.js +144 -0
- package/lib/interpreter-core.js +204 -17
- package/lib/js-compiler-errors.js +52 -16
- package/lib/js-compiler.js +540 -66
- package/lib/keywords.js +24 -1
- package/lib/levenshtein.js +21 -5
- package/lib/metaschemas.js +5 -3
- package/lib/plan-compiler.js +28 -6
- package/lib/regex-linear.js +81 -1
- package/lib/safe-regex.js +14 -11
- package/lib/schema-scan.js +20 -1
- package/lib/tier0.js +1 -0
- package/lib/unique-items.js +16 -5
- package/lib/uri-core.js +155 -0
- package/lib/validator-core.js +118 -23
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/validator-core.js
CHANGED
|
@@ -33,12 +33,13 @@ let _aotLoader = null;
|
|
|
33
33
|
function _registerAot(load) {
|
|
34
34
|
_aotLoader = load;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// draft7.js, vocabularies.js, shape-classifier.js and tier0.js load where
|
|
37
|
+
// they are first used: a fresh process validating a 2020-12 schema runs none
|
|
38
|
+
// of their code, and reading them cost it about a tenth of what it loads.
|
|
39
|
+
const _draft7 = () => require("./draft7");
|
|
40
|
+
const _vocabularies = () => require("./vocabularies");
|
|
38
41
|
const { needsNormalization } = require("./schema-scan");
|
|
39
|
-
const { isV1Dialect } = require("./dialect");
|
|
40
|
-
const { classify } = require("./shape-classifier");
|
|
41
|
-
const { buildTier0Plan, tier0Validate } = require("./tier0");
|
|
42
|
+
const { isV1Dialect, draftCode } = require("./dialect");
|
|
42
43
|
|
|
43
44
|
// The closure defaults pass. Only a validator without the generated
|
|
44
45
|
// preprocess pass calls it, so it loads on that first call; loading it with
|
|
@@ -424,11 +425,22 @@ function _deepCloneWithSymbols(v) {
|
|
|
424
425
|
return a;
|
|
425
426
|
}
|
|
426
427
|
// Only clone plain objects (skip RegExp, Date, etc.).
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
const proto = Object.getPrototypeOf(v);
|
|
429
|
+
if (proto !== Object.prototype && proto !== null) return v;
|
|
430
|
+
// An object literal, filled by assignment: building it without a prototype
|
|
431
|
+
// and setting one at the end left every copy in V8's slow dictionary mode,
|
|
432
|
+
// which made the copy and every walk over it slower. Only a key named
|
|
433
|
+
// `__proto__` needs defineProperty to stay an own property.
|
|
434
|
+
const out = {};
|
|
435
|
+
const keys = Object.keys(v);
|
|
436
|
+
for (let i = 0; i < keys.length; i++) {
|
|
437
|
+
const k = keys[i];
|
|
438
|
+
if (k === '__proto__') Object.defineProperty(out, k, { value: _deepCloneWithSymbols(v[k]), writable: true, enumerable: true, configurable: true });
|
|
439
|
+
else out[k] = _deepCloneWithSymbols(v[k]);
|
|
440
|
+
}
|
|
441
|
+
const syms = Object.getOwnPropertySymbols(v);
|
|
442
|
+
for (let i = 0; i < syms.length; i++) out[syms[i]] = v[syms[i]];
|
|
443
|
+
return out;
|
|
432
444
|
}
|
|
433
445
|
|
|
434
446
|
// Normalize a caller-provided schema without mutating the original.
|
|
@@ -436,11 +448,32 @@ function _deepCloneWithSymbols(v) {
|
|
|
436
448
|
// present or nullable fields present). Internal-only — not exported.
|
|
437
449
|
// `inheritDraft7` is the root's draft for a document that declares none of
|
|
438
450
|
// its own: 0 or absent for none, 1 for draft-07, 2 for draft-06, 3 for 2019-09.
|
|
451
|
+
// Normalized results per schema object and dialect. A registry of hundreds of
|
|
452
|
+
// schemas (an OpenAPI document's components, registered one by one) is
|
|
453
|
+
// normalized again for every validator built over it when the map holding
|
|
454
|
+
// them is a new object each time; the schemas themselves are the same objects,
|
|
455
|
+
// and a schema is not changed after it is handed to a validator.
|
|
456
|
+
const _normalized = new WeakMap()
|
|
439
457
|
function _normalizeCallerSchema(s, inheritDraft7) {
|
|
458
|
+
if (s === null || typeof s !== 'object') return _normalizeCallerSchemaUncached(s, inheritDraft7)
|
|
459
|
+
const slot = inheritDraft7 || 0
|
|
460
|
+
let byDraft = _normalized.get(s)
|
|
461
|
+
if (byDraft !== undefined && byDraft[slot] !== undefined) return byDraft[slot]
|
|
462
|
+
const out = _normalizeCallerSchemaUncached(s, inheritDraft7)
|
|
463
|
+
// Only a copy is kept: a schema that needs nothing is answered by the scan
|
|
464
|
+
// alone, and remembering it cost every such validator a slot.
|
|
465
|
+
if (out !== s) {
|
|
466
|
+
if (byDraft === undefined) { byDraft = [undefined, undefined, undefined, undefined]; _normalized.set(s, byDraft) }
|
|
467
|
+
byDraft[slot] = out
|
|
468
|
+
}
|
|
469
|
+
return out
|
|
470
|
+
}
|
|
471
|
+
function _normalizeCallerSchemaUncached(s, inheritDraft7) {
|
|
440
472
|
const declared = s && typeof s === 'object' ? s.$schema : undefined
|
|
441
473
|
const code = declared !== undefined ? draftCode(declared) : (inheritDraft7 || 0)
|
|
442
|
-
const needsDraft7 = code === 1 || code === 2
|
|
443
|
-
const
|
|
474
|
+
const needsDraft7 = code === 1 || code === 2 || code === 4
|
|
475
|
+
const draft4 = code === 4
|
|
476
|
+
const draft6 = code === 2 || draft4
|
|
444
477
|
const draft2019 = code === 3
|
|
445
478
|
// One walk answers whether there is anything to do. Almost always there is
|
|
446
479
|
// not, and then the serialize, clone, normalize, serialize, compare below is
|
|
@@ -450,17 +483,58 @@ function _normalizeCallerSchema(s, inheritDraft7) {
|
|
|
450
483
|
// The scan does not look for `if`/`then`/`else`, which draft-06 drops.
|
|
451
484
|
if (!draft6 && !draft2019 && !needsNormalization(s, needsDraft7)) return s
|
|
452
485
|
|
|
453
|
-
const str = JSON.stringify(s)
|
|
454
486
|
const copy = _deepCloneWithSymbols(s)
|
|
455
|
-
|
|
487
|
+
const { normalizeDraft7, normalizeDraft2019, normalizeNullable, normalizeExclusiveBounds } = _draft7()
|
|
488
|
+
if (needsDraft7) normalizeDraft7(copy, true, draft6, draft4)
|
|
456
489
|
if (draft2019) normalizeDraft2019(copy, true)
|
|
457
490
|
normalizeNullable(copy)
|
|
458
491
|
normalizeExclusiveBounds(copy)
|
|
459
492
|
// Return original when normalization produced no change, copy otherwise.
|
|
460
493
|
// Kept even though the walk has already said there is work, so that a walk
|
|
461
494
|
// which over-reports still returns exactly what it returned before.
|
|
462
|
-
// Change-detection
|
|
463
|
-
|
|
495
|
+
// Change-detection is JSON content, key order included; symbols do not
|
|
496
|
+
// affect it. Compared in place: serializing both took two passes over the
|
|
497
|
+
// whole document on every compile, most of the compile time of an OpenAPI
|
|
498
|
+
// document that inlines its components into each operation's schema, and
|
|
499
|
+
// a change is usually found in the first few nodes.
|
|
500
|
+
return _jsonEqual(s, copy) ? s : copy
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Whether `a` and `b` serialize to the same JSON, without serializing: the
|
|
504
|
+
// copy is a clone of `a`, so anything the clone shares by reference (a
|
|
505
|
+
// function, a RegExp) is the same on both sides.
|
|
506
|
+
function _jsonEqual (a, b) {
|
|
507
|
+
if (a === b) return true
|
|
508
|
+
if (typeof a === 'number' && typeof b === 'number') return a !== a && b !== b
|
|
509
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
|
|
510
|
+
const arr = Array.isArray(a)
|
|
511
|
+
if (arr !== Array.isArray(b)) return false
|
|
512
|
+
if (arr) {
|
|
513
|
+
if (a.length !== b.length) return false
|
|
514
|
+
for (let i = 0; i < a.length; i++) if (!_jsonEqual(a[i], b[i])) return false
|
|
515
|
+
return true
|
|
516
|
+
}
|
|
517
|
+
const ka = _jsonKeys(a), kb = _jsonKeys(b)
|
|
518
|
+
if (ka.length !== kb.length) return false
|
|
519
|
+
for (let i = 0; i < ka.length; i++) {
|
|
520
|
+
if (ka[i] !== kb[i]) return false
|
|
521
|
+
if (!_jsonEqual(a[ka[i]], b[kb[i]])) return false
|
|
522
|
+
}
|
|
523
|
+
return true
|
|
524
|
+
}
|
|
525
|
+
// The keys JSON.stringify writes: own enumerable ones whose value is not
|
|
526
|
+
// undefined, a function or a symbol.
|
|
527
|
+
function _jsonKeys (o) {
|
|
528
|
+
const keys = Object.keys(o)
|
|
529
|
+
let out = null
|
|
530
|
+
for (let i = 0; i < keys.length; i++) {
|
|
531
|
+
const v = o[keys[i]]
|
|
532
|
+
const t = typeof v
|
|
533
|
+
if (v === undefined || t === 'function' || t === 'symbol') {
|
|
534
|
+
if (out === null) out = keys.slice(0, i)
|
|
535
|
+
} else if (out !== null) out.push(keys[i])
|
|
536
|
+
}
|
|
537
|
+
return out === null ? keys : out
|
|
464
538
|
}
|
|
465
539
|
|
|
466
540
|
// The identity a document is registered under. Draft-07 ignores every
|
|
@@ -544,7 +618,11 @@ function _applyVocabularies(schemaObj, original, schemaMap) {
|
|
|
544
618
|
if (!schemaObj || typeof schemaObj !== 'object') return schemaObj
|
|
545
619
|
const declared = schemaObj.$schema
|
|
546
620
|
if (typeof declared !== 'string') return schemaObj
|
|
547
|
-
|
|
621
|
+
// Only a meta-schema with a $vocabulary can disable a keyword.
|
|
622
|
+
const meta = schemaMap.get(declared)
|
|
623
|
+
if (!meta || typeof meta !== 'object' || !meta.$vocabulary || typeof meta.$vocabulary !== 'object') return schemaObj
|
|
624
|
+
const { enabledKeywords, stripDisabledKeywords } = _vocabularies()
|
|
625
|
+
const enabled = enabledKeywords(meta)
|
|
548
626
|
if (!enabled) return schemaObj
|
|
549
627
|
// `original` is the caller's own object when it reached here unchanged, and
|
|
550
628
|
// that one is never mutated.
|
|
@@ -650,7 +728,10 @@ function _materializeSchema(self) {
|
|
|
650
728
|
}
|
|
651
729
|
const isCallers = self._rawIsCallers && schemaObj === raw;
|
|
652
730
|
if (options.assertFormat === false) {
|
|
653
|
-
|
|
731
|
+
// Always on a copy: a normalized schema is shared by every validator built
|
|
732
|
+
// from the same object (see _normalizeCallerSchema), so stripping in place
|
|
733
|
+
// would take format checks away from the others.
|
|
734
|
+
schemaObj = _draft7().stripFormatAssertions(_deepCloneWithSymbols(schemaObj));
|
|
654
735
|
}
|
|
655
736
|
// A keyword used only in a schema reached through $ref counts too: the
|
|
656
737
|
// interpreted engine applies it there, and every path that does not know the
|
|
@@ -952,7 +1033,10 @@ class Validator {
|
|
|
952
1033
|
// declines the schema, so does everything else, and the interpreted
|
|
953
1034
|
// engine answers alone, as it always did.
|
|
954
1035
|
const kw = this._keywords ? { keywords: this._keywords } : undefined;
|
|
955
|
-
|
|
1036
|
+
// A keyword that reads its data's context is only answered by the
|
|
1037
|
+
// interpreted engine's collecting walk (see lib/keywords.js).
|
|
1038
|
+
const ctxKw = this._usesKeywords && Object.keys(this._keywords).some((k) => this._keywords[k].dataContext);
|
|
1039
|
+
const _cgFn = ctxKw ? null : compileToJSCodegen(schemaObj, sm, uf, kw);
|
|
956
1040
|
jsFn = _cgFn || (this._usesKeywords ? null : compileToJS(schemaObj, null, sm));
|
|
957
1041
|
// Only the verdict is compiled here. The error and combined generators
|
|
958
1042
|
// are the other two thirds of a cold first call (8.2, 10.4 and 7.8 ms on
|
|
@@ -1119,9 +1203,18 @@ class Validator {
|
|
|
1119
1203
|
formats: this._userFormats,
|
|
1120
1204
|
v1: isV1Dialect(schemaObj),
|
|
1121
1205
|
keywords: this._keywords,
|
|
1206
|
+
// compat's coercion where each type is checked (see the interpreter).
|
|
1207
|
+
coerce: this._interpretOnly ? options._coerceInWalk : undefined,
|
|
1208
|
+
discriminator: this._interpretOnly && options._discriminator === true,
|
|
1209
|
+
numberFormats: this._interpretOnly ? options._numberFormats : undefined,
|
|
1122
1210
|
});
|
|
1123
1211
|
this._engine = 'interpreter';
|
|
1124
|
-
|
|
1212
|
+
// A keyword that reads its data's context may also change the data (a
|
|
1213
|
+
// request validator deserializing a date, say). Such a schema is run
|
|
1214
|
+
// once per call: deciding first and walking again for the errors would
|
|
1215
|
+
// run the keyword twice, the second time on what the first left.
|
|
1216
|
+
const once = interp.dataContext === true;
|
|
1217
|
+
if (!preprocess && !once) this._fastVerdict = (d) => interp.isValid(d);
|
|
1125
1218
|
// abortEarly is a documented contract, not a property of whichever engine
|
|
1126
1219
|
// answered: it promises the frozen ATA9000 stub instead of a detailed
|
|
1127
1220
|
// error, so code that branches on it has to behave the same with and
|
|
@@ -1136,7 +1229,7 @@ class Validator {
|
|
|
1136
1229
|
: (data) => interp.validate(data));
|
|
1137
1230
|
this.validate = this._verbose ? _verboseWrap(run, schemaObj) : run;
|
|
1138
1231
|
// Reading errors runs the interpreter once, directly; see _rejectBase.
|
|
1139
|
-
if (!preprocess && !options.abortEarly && !this._verbose) _rejectBase = (data) => interp.validate(data);
|
|
1232
|
+
if (!preprocess && !options.abortEarly && !this._verbose && !once) _rejectBase = (data) => interp.validate(data);
|
|
1140
1233
|
_bindVerdict(this, this._fastVerdict
|
|
1141
1234
|
? this._fastVerdict
|
|
1142
1235
|
: (data) => run(data).valid);
|
|
@@ -1146,7 +1239,7 @@ class Validator {
|
|
|
1146
1239
|
// that the text path wraps around it: going through validate() as it
|
|
1147
1240
|
// stands once every layer is in place handed the text path errors
|
|
1148
1241
|
// already presented, and they lost their source anchor.
|
|
1149
|
-
const runText = options.abortEarly
|
|
1242
|
+
const runText = options.abortEarly || once
|
|
1150
1243
|
? run
|
|
1151
1244
|
: (preprocess
|
|
1152
1245
|
? (data) => { preprocess(data); return interp.isValid(data) ? VALID_RESULT : interp.validate(data); }
|
|
@@ -2309,8 +2402,10 @@ _defineLazyMethod('isValidObject', (self) => {
|
|
|
2309
2402
|
return self.isValidObject(data);
|
|
2310
2403
|
}
|
|
2311
2404
|
// Lazy: classify + build tier 0 plan on first call, not in constructor.
|
|
2312
|
-
const _tier = classify(self._schemaObj);
|
|
2405
|
+
const _tier = require("./shape-classifier").classify(self._schemaObj);
|
|
2313
2406
|
if (_tier.tier === 0) {
|
|
2407
|
+
// Bound once here, so the verdict below reads a local, not the module.
|
|
2408
|
+
const { buildTier0Plan, tier0Validate } = require("./tier0");
|
|
2314
2409
|
const _plan = buildTier0Plan(self._schemaObj);
|
|
2315
2410
|
let _n = 0;
|
|
2316
2411
|
_bindVerdict(self, (d) => {
|
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.54.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",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"rebuild": "cmake-js rebuild --target ata",
|
|
89
89
|
"test": "node tests/run.js",
|
|
90
90
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
91
|
-
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1 && node tests/run_suite.js draft6 && node tests/run_suite.js draft2019-09",
|
|
91
|
+
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1 && node tests/run_suite.js draft6 && node tests/run_suite.js draft2019-09 && node tests/run_suite.js draft4",
|
|
92
92
|
"test:compat": "node tests/test_compat.js",
|
|
93
93
|
"test:standard-schema": "node tests/test_standard_schema.js",
|
|
94
94
|
"test:browser": "node tests/test_browser.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.54.0",
|
|
178
|
+
"@ata-validator/native-darwin-x64": "1.54.0",
|
|
179
|
+
"@ata-validator/native-linux-arm64-gnu": "1.54.0",
|
|
180
|
+
"@ata-validator/native-linux-arm64-musl": "1.54.0",
|
|
181
|
+
"@ata-validator/native-linux-x64-gnu": "1.54.0",
|
|
182
|
+
"@ata-validator/native-linux-x64-musl": "1.54.0",
|
|
183
|
+
"@ata-validator/native-win32-x64": "1.54.0"
|
|
184
184
|
},
|
|
185
185
|
"peerDependencies": {
|
|
186
186
|
"yaml": "^2.0.0"
|