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.
@@ -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
- if (!((prop.fn.leaf && prop.fn.v(data[key], stack)) || prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack))) {
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
- if (!propList[i].fn.v(data[key], stack)) return false;
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
- if (!prop.fn.v(data[key], stack)) return false;
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) {
@@ -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++) walk(node[keys[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
  };
@@ -424,11 +424,22 @@ function _deepCloneWithSymbols(v) {
424
424
  return a;
425
425
  }
426
426
  // 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);
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 uses JSON content only; symbols do not affect it.
463
- return JSON.stringify(copy) === str ? s : copy
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
- schemaObj = stripFormatAssertions(isCallers ? _deepCloneWithSymbols(schemaObj) : schemaObj);
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
- const _cgFn = compileToJSCodegen(schemaObj, sm, uf, kw);
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
- if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
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
@@ -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.53.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.52.0",
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.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.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"