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.
@@ -33,12 +33,13 @@ let _aotLoader = null;
33
33
  function _registerAot(load) {
34
34
  _aotLoader = load;
35
35
  }
36
- const { draftCode, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
37
- const { enabledKeywords, stripDisabledKeywords } = require("./vocabularies");
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
- if (Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) return v;
428
- const out = Object.create(null);
429
- for (const k of Object.keys(v)) Object.defineProperty(out, k, { value: _deepCloneWithSymbols(v[k]), writable: true, enumerable: true, configurable: true });
430
- for (const sym of Object.getOwnPropertySymbols(v)) out[sym] = v[sym];
431
- return Object.setPrototypeOf(out, Object.prototype);
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 draft6 = code === 2
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
- if (needsDraft7) normalizeDraft7(copy, true, draft6)
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 uses JSON content only; symbols do not affect it.
463
- return JSON.stringify(copy) === str ? s : copy
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
- const enabled = enabledKeywords(schemaMap.get(declared))
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
- schemaObj = stripFormatAssertions(isCallers ? _deepCloneWithSymbols(schemaObj) : schemaObj);
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
- const _cgFn = compileToJSCodegen(schemaObj, sm, uf, kw);
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
- if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
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
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.52.0';
10
+ module.exports = '1.54.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.52.0",
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.52.0",
178
- "@ata-validator/native-darwin-x64": "1.52.0",
179
- "@ata-validator/native-linux-arm64-gnu": "1.52.0",
180
- "@ata-validator/native-linux-arm64-musl": "1.52.0",
181
- "@ata-validator/native-linux-x64-gnu": "1.52.0",
182
- "@ata-validator/native-linux-x64-musl": "1.52.0",
183
- "@ata-validator/native-win32-x64": "1.52.0"
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"