ata-validator 1.62.1 → 1.63.1

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.
Files changed (53) hide show
  1. package/README.md +63 -8
  2. package/build.mjs +1 -0
  3. package/index.browser.mjs +23 -4
  4. package/index.bundle.mjs +14 -4
  5. package/index.edge.mjs +14 -4
  6. package/index.js +7 -2
  7. package/lib/aot-build.js +18 -0
  8. package/lib/codegen-engine.js +72 -3
  9. package/lib/compiled-common.js +2 -1
  10. package/lib/email-literal.js +4 -1
  11. package/lib/formats-source.js +39 -40
  12. package/lib/interpreter-core.js +148 -70
  13. package/lib/ir/emit-deps.mjs +131 -0
  14. package/lib/ir/emit.mjs +707 -0
  15. package/lib/ir/front.mjs +865 -0
  16. package/lib/ir/index.mjs +10 -0
  17. package/lib/ir/refs.mjs +108 -0
  18. package/lib/ir/run.mjs +520 -0
  19. package/lib/ir/table.mjs +233 -0
  20. package/lib/js-compiler-errors.js +9 -4
  21. package/lib/kernels/collapse.mjs +114 -0
  22. package/lib/kernels/custom.mjs +70 -0
  23. package/lib/kernels/fill.mjs +6 -0
  24. package/lib/kernels/formats-small.mjs +40 -0
  25. package/lib/kernels/formats.mjs +29 -0
  26. package/lib/kernels/index.mjs +11 -0
  27. package/lib/kernels/kernels.mjs +285 -0
  28. package/lib/kernels/leaves.mjs +28 -0
  29. package/lib/kernels/order.mjs +25 -0
  30. package/lib/kernels/plan-rich.mjs +17 -0
  31. package/lib/kernels/plan.d.ts +9 -0
  32. package/lib/kernels/plan.mjs +128 -0
  33. package/lib/kernels/preprocess.mjs +223 -0
  34. package/lib/kernels/rank.mjs +97 -0
  35. package/lib/kernels/rt.mjs +249 -0
  36. package/lib/kernels/table.mjs +182 -0
  37. package/lib/kernels/unique.mjs +157 -0
  38. package/lib/native-positions.js +1 -1
  39. package/lib/openapi.d.ts +2 -0
  40. package/lib/openapi.js +128 -0
  41. package/lib/plan-compiler.js +113 -14
  42. package/lib/public-api.js +7 -2
  43. package/lib/rejections.js +3 -1
  44. package/lib/schema-helpers.js +12 -0
  45. package/lib/schema-helpers.mjs +20 -0
  46. package/lib/schema-order.js +4 -2
  47. package/lib/shape-classifier.js +13 -1
  48. package/lib/standard-schema.js +1 -1
  49. package/lib/tier0.js +38 -8
  50. package/lib/validator-core.js +59 -36
  51. package/lib/version.js +1 -1
  52. package/lite.js +7 -2
  53. package/package.json +26 -8
@@ -56,6 +56,21 @@ function install(deps) {
56
56
  T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
57
57
 
58
58
  const TRUE_FN = () => true;
59
+ // Leaf codes (leafCode in compileLeafV): a string with lengths and a
60
+ // pattern, an integer or a number with minimum and maximum, a boolean, an
61
+ // enum of primitives, a primitive const. Each answers as the closure it
62
+ // stands for.
63
+ const LC_STR = 1, LC_INT = 2, LC_NUM = 3, LC_BOOL = 4, LC_ENUM = 5, LC_CONST = 6;
64
+ function leafOk(c, d) {
65
+ switch (c[0]) {
66
+ case LC_STR: return typeof d === 'string' && (c[1] === -1 || cpAtLeast(d, c[1])) && (c[2] === -1 || cpAtMost(d, c[2])) && (c[3] === null || c[3].test(d));
67
+ case LC_INT: case LC_NUM: return (c[0] === LC_INT ? Number.isInteger(d) : typeof d === 'number' && isFinite(d)) && d >= c[1] && d <= c[2];
68
+ case LC_BOOL: return typeof d === 'boolean';
69
+ case LC_ENUM: return c[1].has(d);
70
+ default: return d === c[1];
71
+ }
72
+ }
73
+
59
74
  // `leaf`: the node compiled no subschema of its own (see compileNode). A
60
75
  // collecting caller asks a leaf for its verdict first and builds the paths
61
76
  // for its errors only when it fails.
@@ -92,6 +107,7 @@ function install(deps) {
92
107
  function fresh() { return { props: null, n: 0, x: null, keys: null }; }
93
108
  // Object.keys once per record: every node that shares a record sees the
94
109
  // same data object.
110
+ const propEnum = Object.prototype.propertyIsEnumerable;
95
111
  function keysOf(rec, data) {
96
112
  if (rec.keys === null) rec.keys = Object.keys(data);
97
113
  return rec.keys;
@@ -183,11 +199,17 @@ function install(deps) {
183
199
  // chain of guarded checks: the interpreted engine spent a fifth of a
184
200
  // document in those calls.
185
201
  const reqFolded = P.required !== null && P.requiredUndef === null && P.properties !== null;
202
+ // The operands of the commonest leaves, for an object walk that checks
203
+ // them in place (leafCode below): bounds that are only minimum and
204
+ // maximum, and string checks that are only lengths and a pattern.
205
+ let nLo = -Infinity, nHi = Infinity, nPlain = true;
206
+ let sLo = -1, sHi = -1, sRe = null, sPlain = true;
186
207
  let numsRun = null;
187
208
  if (P.hasNumber) {
188
209
  const nums = [];
189
- if (P.minimum !== undefined) { const m = P.minimum; nums.push((d) => d >= m); }
190
- if (P.maximum !== undefined) { const m = P.maximum; nums.push((d) => d <= m); }
210
+ if (P.minimum !== undefined) { const m = P.minimum; nLo = m; nums.push((d) => d >= m); }
211
+ if (P.maximum !== undefined) { const m = P.maximum; nHi = m; nums.push((d) => d <= m); }
212
+ if (P.exclusiveMinimum !== undefined || P.exclusiveMaximum !== undefined || P.multipleOf !== undefined) nPlain = false;
191
213
  if (P.exclusiveMinimum !== undefined) { const m = P.exclusiveMinimum; nums.push((d) => d > m); }
192
214
  if (P.exclusiveMaximum !== undefined) { const m = P.exclusiveMaximum; nums.push((d) => d < m); }
193
215
  if (P.multipleOf !== undefined) { const m = P.multipleOf; nums.push((d) => multipleOfOk(d, m)); }
@@ -196,6 +218,10 @@ function install(deps) {
196
218
  let strsRun = null;
197
219
  if (P.hasString) {
198
220
  const strs = [];
221
+ if (P.minLength !== undefined) sLo = P.minLength;
222
+ if (P.maxLength !== undefined) sHi = P.maxLength;
223
+ if (P.pattern !== null) sRe = P.pattern;
224
+ if (P.formatFn !== null) sPlain = false;
199
225
  if (P.minLength !== undefined && P.maxLength !== undefined) {
200
226
  const lo = P.minLength, hi = P.maxLength;
201
227
  strs.push((d) => { const n = d.length; return (n >= 2 * lo && n <= hi) || (cpAtLeast(d, lo) && cpAtMost(d, hi)); });
@@ -254,10 +280,10 @@ function install(deps) {
254
280
  if (!foreign) {
255
281
  const r = own;
256
282
  switch (m) {
257
- case T_STRING: return r === null ? (d) => typeof d === 'string' : (d) => typeof d === 'string' && r(d);
258
- case T_INTEGER: return r === null ? (d) => Number.isInteger(d) : (d) => Number.isInteger(d) && r(d);
259
- case T_NUMBER: return r === null ? (d) => typeof d === 'number' && isFinite(d) : (d) => typeof d === 'number' && isFinite(d) && r(d);
260
- case T_BOOLEAN: return (d) => typeof d === 'boolean';
283
+ case T_STRING: return leafCode(r === null ? (d) => typeof d === 'string' : (d) => typeof d === 'string' && r(d), sPlain ? [LC_STR, sLo, sHi, sRe] : null);
284
+ case T_INTEGER: return leafCode(r === null ? (d) => Number.isInteger(d) : (d) => Number.isInteger(d) && r(d), nPlain ? [LC_INT, nLo, nHi] : null);
285
+ case T_NUMBER: return leafCode(r === null ? (d) => typeof d === 'number' && isFinite(d) : (d) => typeof d === 'number' && isFinite(d) && r(d), nPlain ? [LC_NUM, nLo, nHi] : null);
286
+ case T_BOOLEAN: return leafCode((d) => typeof d === 'boolean', [LC_BOOL]);
261
287
  case T_NULL: return (d) => d === null;
262
288
  case T_OBJECT: return r === null ? (d) => typeof d === 'object' && d !== null && !Array.isArray(d) : (d) => typeof d === 'object' && d !== null && !Array.isArray(d) && r(d);
263
289
  case T_ARRAY: return r === null ? (d) => Array.isArray(d) : (d) => Array.isArray(d) && r(d);
@@ -279,7 +305,16 @@ function install(deps) {
279
305
  }
280
306
  if (checks.length === 0) return TRUE_FN;
281
307
  if (!needsBits) {
282
- if (checks.length === 1) { const a = checks[0]; return (data) => a(data, 0); }
308
+ if (checks.length === 1) {
309
+ const a = checks[0];
310
+ const fn = (data) => a(data, 0);
311
+ // An enum of primitives alone, or a primitive const alone.
312
+ if (!P.hasType && !P.hasCustom && numsRun === null && strsRun === null && arrsRun === null && objsRun === null) {
313
+ if (P.enum !== null && P.enum.length > 0 && P.enumObjs.length === 0) return leafCode(fn, [LC_ENUM, P.enumPrims]);
314
+ if (P.hasConst && P.enum === null && (P.const === null || typeof P.const !== 'object')) return leafCode(fn, [LC_CONST, P.const]);
315
+ }
316
+ return fn;
317
+ }
283
318
  return (data) => {
284
319
  for (let i = 0; i < checks.length; i++) if (!checks[i](data, 0)) return false;
285
320
  return true;
@@ -297,6 +332,16 @@ function install(deps) {
297
332
  };
298
333
  }
299
334
 
335
+ // A leaf check that an object walk may run in place, by its code and
336
+ // operands, instead of calling it: one call site calling every property's
337
+ // check cannot be inlined, and on a form the calls were most of the
338
+ // verdict. leafOk must answer as the function does; tests/test_plan_leaf_codes.js
339
+ // holds the two to each other.
340
+ function leafCode(fn, code) {
341
+ if (code !== null) fn.lc = code;
342
+ return fn;
343
+ }
344
+
300
345
  // Folds a group's checks into one call, or null when the group is empty.
301
346
  function combineValue(fns) {
302
347
  if (fns.length === 0) return null;
@@ -565,9 +610,10 @@ function install(deps) {
565
610
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
566
611
  if (dataBits(data) !== T_OBJECT) return true;
567
612
  let ok = true;
568
- const keys = collect ? keysOf(rec, data) : Object.keys(data);
613
+ const keys = collect ? keysOf(rec, data) : Object.getOwnPropertyNames(data);
569
614
  if (pnFn !== null) {
570
615
  for (const k of keys) {
616
+ if (!collect && !propEnum.call(data, k)) continue;
571
617
  // propertyNames applies its subschema to the property NAME, not to
572
618
  // the value sitting at that key, so the failure is reported against
573
619
  // the object that carries the name rather than against `/<key>`.
@@ -599,7 +645,11 @@ function install(deps) {
599
645
  }
600
646
  evaluated = true;
601
647
  }
602
- if (patterns !== null) {
648
+ // A name only properties lists is read whether enumerable or not,
649
+ // as the short walk above and the generated code read it by name;
650
+ // patterns and additionalProperties see the names for-in sees.
651
+ const seen = collect || prop !== undefined && patterns === null && apFn === null || propEnum.call(data, key);
652
+ if (patterns !== null && seen) {
603
653
  for (let pi = 0; pi < patterns.length; pi++) {
604
654
  const pp = patterns[pi];
605
655
  if (pp.re.test(key)) {
@@ -611,7 +661,7 @@ function install(deps) {
611
661
  }
612
662
  }
613
663
  }
614
- if (!evaluated && apFn !== null) {
664
+ if (!evaluated && apFn !== null && seen) {
615
665
  if (apFalse) {
616
666
  ok = false;
617
667
  if (errors === NOERRORS) return false;
@@ -671,9 +721,9 @@ function install(deps) {
671
721
  vsteps.push((data, stack, rec) => {
672
722
  if (fusedLeaf !== null && !fusedLeaf(data)) return false;
673
723
  if (dataBits(data) !== T_OBJECT) return true;
674
- const keys = collect ? keysOf(rec, data) : Object.keys(data);
724
+ const keys = collect ? keysOf(rec, data) : Object.getOwnPropertyNames(data);
675
725
  if (pnFn !== null) {
676
- for (const k of keys) if (!pnFn.v(k, stack)) return false;
726
+ for (const k of keys) if ((collect || propEnum.call(data, k)) && !pnFn.v(k, stack)) return false;
677
727
  }
678
728
  let seenReq = 0;
679
729
  for (let k = 0; k < keys.length; k++) {
@@ -687,7 +737,8 @@ function install(deps) {
687
737
  if (prop.req) seenReq++;
688
738
  evaluated = true;
689
739
  }
690
- if (patterns !== null) {
740
+ const seen = collect || prop !== undefined && patterns === null && apFn === null || propEnum.call(data, key);
741
+ if (patterns !== null && seen) {
691
742
  for (let pi = 0; pi < patterns.length; pi++) {
692
743
  const pp = patterns[pi];
693
744
  if (pp.re.test(key)) {
@@ -696,7 +747,7 @@ function install(deps) {
696
747
  }
697
748
  }
698
749
  }
699
- if (!evaluated && apFn !== null) {
750
+ if (!evaluated && apFn !== null && seen) {
700
751
  if (!apFn.v(data[key], stack)) return false;
701
752
  evaluated = true;
702
753
  }
@@ -704,6 +755,54 @@ function install(deps) {
704
755
  }
705
756
  return seenReq === reqN;
706
757
  });
758
+ // From three names to 32: below three the key array costs less than
759
+ // the for-in (1.7 ns on one name), past 32 the scan costs more.
760
+ if (apFalse && patterns === null && pnFn === null && !collect && propKeys !== null && propKeys.length >= 3 && propKeys.length <= 32) {
761
+ // A closed object with a short list of names, on an ordinary
762
+ // object: walk its keys with for-in and find each in the list, no
763
+ // key array and no Map. A key the list lacks is extra when it is
764
+ // the object's own (an enumerable name on Object.prototype is not).
765
+ // A listed key is read directly: for-in meets Object.prototype's
766
+ // own names (`constructor`) only on the object itself, so this is
767
+ // the short walk's rule. Other objects take the walk above.
768
+ const keyed = vsteps.pop();
769
+ const n = propKeys.length;
770
+ const fns = propList.map((e) => e.fn);
771
+ const reqs = propList.map((e) => e.req);
772
+ // Names met, as bits: the required ones must all be set, and a
773
+ // listed name not met may still be the object's own, hidden from
774
+ // for-in (defineProperty, enumerable: false), which the generated
775
+ // code and this engine's error path read by name: those are read
776
+ // after the walk, which on an ordinary document is the absent ones.
777
+ const reqMask = reqs.reduce((m, r, i) => r ? m | (1 << i) : m, 0);
778
+ const allMask = n === 32 ? -1 : (1 << n) - 1;
779
+ // A property whose check is a coded leaf is checked in place.
780
+ const codes = propList.map((e) => (e.fn.leaf && e.fn.v.lc !== undefined ? e.fn.v.lc : null));
781
+ vsteps.push((data, stack, rec) => {
782
+ if (dataBits(data) !== T_OBJECT || Object.getPrototypeOf(data) !== Object.prototype) return keyed(data, stack, rec);
783
+ if (fusedLeaf !== null && !fusedLeaf(data)) return false;
784
+ let seen = 0;
785
+ for (const key in data) {
786
+ let i = 0;
787
+ while (i < n && propKeys[i] !== key) i++;
788
+ if (i === n) { if (Object.hasOwn(data, key)) return false; continue; }
789
+ seen |= 1 << i;
790
+ const value = data[key];
791
+ if (value === undefined) { if (reqs[i]) return false; continue; }
792
+ const lc = codes[i];
793
+ if (lc !== null ? !leafOk(lc, value) : !fns[i].v(value, stack)) return false;
794
+ }
795
+ if ((seen & reqMask) !== reqMask) return false;
796
+ for (let miss = seen ^ allMask; miss !== 0; miss &= miss - 1) {
797
+ const i = 31 - Math.clz32(miss & -miss);
798
+ const value = data[propKeys[i]];
799
+ if (value === undefined || !Object.hasOwn(data, propKeys[i])) continue;
800
+ const lc = codes[i];
801
+ if (lc !== null ? !leafOk(lc, value) : !fns[i].v(value, stack)) return false;
802
+ }
803
+ return true;
804
+ });
805
+ }
707
806
  }
708
807
  }
709
808
  if (P.dependentSchemas !== null) {
package/lib/public-api.js CHANGED
@@ -58,6 +58,11 @@ function attachSuggestions (errors, data) {
58
58
  return errors;
59
59
  }
60
60
 
61
+ // Loaded on first call, like the tools: a process that never calls them does
62
+ // not read the file.
63
+ function defineSchema (schema) { return require('./schema-helpers.js').defineSchema(schema); }
64
+ function schemaOf (...a) { return require('./schema-helpers.js').schemaOf(...a); }
65
+
61
66
  module.exports = {
62
67
  Validator: core.Validator,
63
68
  compile: core.compile,
@@ -69,8 +74,8 @@ module.exports = {
69
74
  SIMDJSON_PADDING: core.SIMDJSON_PADDING,
70
75
  parseJSON: core.parseJSON,
71
76
  toTypeScript,
72
- defineSchema: core.defineSchema,
73
- schemaOf: core.schemaOf,
77
+ defineSchema,
78
+ schemaOf,
74
79
  renderPretty,
75
80
  renderCompact,
76
81
  toOutput,
package/lib/rejections.js CHANGED
@@ -159,7 +159,9 @@ function stripOrdinal(e) {
159
159
  // lib/branch-collapse.js builds, the only place these codes come from. Its
160
160
  // fields are copied by name, in that order, without the ordinal: walking
161
161
  // its keys with for-in was most of the cost of reading such an error.
162
- if (e.path !== undefined && e.docUrl === undefined && COLLAPSE_CODES.has(e.code)) {
162
+ // One that carries verbose's fields takes the copy of every key below,
163
+ // which keeps them; this one dropped them.
164
+ if (e.path !== undefined && e.docUrl === undefined && COLLAPSE_CODES.has(e.code) && !('parentSchema' in e)) {
163
165
  const out = { code: e.code, keyword: e.keyword, instancePath: e.instancePath, path: e.path, schemaPath: e.schemaPath, message: e.message, params: e.params };
164
166
  if ('branchErrors' in e) out.branchErrors = Array.isArray(e.branchErrors) ? e.branchErrors.map(stripOrdinal) : e.branchErrors;
165
167
  return out;
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ // defineSchema and schemaOf outside the core; lib/schema-helpers.mjs says why.
4
+ function defineSchema (schema) {
5
+ return schema;
6
+ }
7
+
8
+ function schemaOf () {
9
+ throw new Error('schemaOf<T>() is replaced at build time by @ata-project/unplugin (with ts-json-schema-generator installed); it has no run-time form. Write the JSON Schema, or build with the plugin.');
10
+ }
11
+
12
+ module.exports = { defineSchema, schemaOf };
@@ -0,0 +1,20 @@
1
+ // The ES form of lib/schema-helpers.js, for the ES entries. Those entries read
2
+ // each core name through a call marked pure, so a module that imports only
3
+ // `defineSchema` leaves the validator core out of its bundle; a destructuring
4
+ // `export const { ... } = core` was kept whole, and a form whose validator the
5
+ // build plugin had compiled away still shipped the runtime through it. The core
6
+ // holds no copy, so a bundle carries the helpers once.
7
+ // tests/test_define_schema_bundle.js covers both forms.
8
+
9
+ // Identity at run time: it gives an inline schema its JSONSchema type
10
+ // (index.d.ts) for autocomplete and value checks while authoring.
11
+ export function defineSchema (schema) {
12
+ return schema;
13
+ }
14
+
15
+ // Written in by @ata-project/unplugin from a TypeScript type at build time. A
16
+ // call that reaches run time was not replaced, and a loud error beats an
17
+ // empty schema that accepts everything.
18
+ export function schemaOf () {
19
+ throw new Error('schemaOf<T>() is replaced at build time by @ata-project/unplugin (with ts-json-schema-generator installed); it has no run-time form. Write the JSON Schema, or build with the plugin.');
20
+ }
@@ -140,8 +140,10 @@ function computeRank(rootSchema, schemaPath) {
140
140
  function remoteTarget(docs, base, node) {
141
141
  const ref = node.$ref;
142
142
  if (typeof ref !== 'string' || ref.charCodeAt(0) === 35) return null;
143
- const abs = resolveUri(ref, base);
144
- if (abs === null) return null;
143
+ // A name with no base to resolve against (a root without $id referencing
144
+ // `shared#`) is looked up as written, the way such documents are registered
145
+ // (schemas: [{ $id: 'shared' }]); it used to stop the rank there.
146
+ const abs = resolveUri(ref, base) || ref;
145
147
  const hash = abs.indexOf('#');
146
148
  const uri = hash < 0 ? abs : abs.slice(0, hash);
147
149
  let target = docs.get(uri);
@@ -40,9 +40,21 @@ function isPrimitiveEnumValue(v) {
40
40
  return v === null || t === 'string' || t === 'number' || t === 'boolean';
41
41
  }
42
42
 
43
+ // A node of `enum` or `const` alone, no type: the values decide, as the full
44
+ // engines decide it (enumMember, deepEqual), and every value is a primitive,
45
+ // so a Set lookup or a === is the whole check. Form fields are written this
46
+ // way often (`{ enum: ['TR', 'DE'] }`, `{ const: true }`), and one of them
47
+ // sent the whole object to the slower engines.
48
+ const TIER0_UNTYPED_ALLOWED = new Set(['enum', 'const', ...META_KEYS]);
49
+
43
50
  function isTier0Primitive(schema) {
44
51
  if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return false;
45
- if (!isPrimitiveType(schema.type)) return false;
52
+ if (!isPrimitiveType(schema.type)) {
53
+ if (schema.type !== undefined || (schema.enum === undefined && schema.const === undefined)) return false;
54
+ for (const k of Object.keys(schema)) {
55
+ if (!TIER0_UNTYPED_ALLOWED.has(k)) return false;
56
+ }
57
+ }
46
58
  for (const k of Object.keys(schema)) {
47
59
  if (!TIER0_PRIMITIVE_ALLOWED.has(k)) return false;
48
60
  }
@@ -117,7 +117,7 @@ function standardSchemaFor(self, getSchema, validate) {
117
117
  if (!message) {
118
118
  // The native engine reports numeric codes without a message; the
119
119
  // enrich pass knows how to word those. Rare, so required lazily.
120
- message = require('./native-message')(err);
120
+ { const m = require('./native-message'); message = (typeof m === 'function' ? m : m.default)(err); }
121
121
  }
122
122
  issues[i] = { message, path: parsePointerPath(path) };
123
123
  }
package/lib/tier0.js CHANGED
@@ -166,9 +166,10 @@ function checkPrimitive(c, v) {
166
166
  }
167
167
  } else if (m === T_BOOLEAN) {
168
168
  if (typeof v !== 'boolean') return false;
169
- } else {
169
+ } else if (m !== 0) {
170
170
  return false;
171
171
  }
172
+ // m === 0: a node of enum or const alone; the checks below decide.
172
173
  if (c.hasEnum && !c.enumSet.has(v)) return false;
173
174
  if (c.hasConst && v !== c.constVal) return false;
174
175
  return true;
@@ -176,18 +177,32 @@ function checkPrimitive(c, v) {
176
177
 
177
178
  // Inlined object validator. Separating the primitive path removes a dead
178
179
  // branch from the hot object path.
179
- function tier0ValidateObject(plan, data) {
180
+ // The object verdict as one closure over the plan's fields, read once: the
181
+ // bound isValidObject() is then one call into a function whose loop reads
182
+ // captured constants, where a plan passed each call cost two calls and a
183
+ // field load per use (an empty object 17 ns against 7 for a validator built
184
+ // the same way by hand; a number 9 against 4).
185
+ function makeObjectVerdict(plan) {
186
+ const P_additionalAllowed = plan.additionalAllowed;
187
+ const P_constraints = plan.constraints;
188
+ const P_knownKeys = plan.knownKeys;
189
+ const P_requiredMask = plan.requiredMask;
190
+ return function tier0ObjectVerdict(data) {
180
191
  if (typeof data !== 'object' || data === null || Array.isArray(data)) return false;
181
- const cs = plan.constraints;
192
+ const cs = P_constraints;
182
193
  const n = cs.length;
183
- const reqMask = plan.requiredMask;
194
+ const reqMask = P_requiredMask;
195
+ // Whether the object is an ordinary one, read once: then a defined value
196
+ // under a name Object.prototype lacks is the object's own. Read for each
197
+ // present field, it was one test per field on a valid document.
198
+ const plain = (_PN ? data.__proto__ : Object.getPrototypeOf(data)) === Object.prototype;
184
199
  let seenMask = 0;
185
200
  for (let i = 0; i < n; i++) {
186
201
  const c = cs[i];
187
202
  let v = data[c.key];
188
203
  // Presence is an own-property question: a value the object inherits is
189
204
  // treated as absent, as the interpreted engine treats it.
190
- if (v !== undefined && (c.protoName || (_PN ? data.__proto__ : Object.getPrototypeOf(data)) !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
205
+ if (v !== undefined && (c.protoName || !plain) && !_hop.call(data, c.key)) v = undefined;
191
206
  if (v === undefined) {
192
207
  // A required key holding undefined is missing, as in every engine.
193
208
  if (reqMask & (1 << i)) return false;
@@ -225,21 +240,35 @@ function tier0ValidateObject(plan, data) {
225
240
  }
226
241
  } else if (m === T_BOOLEAN) {
227
242
  if (typeof v !== 'boolean') return false;
228
- } else {
243
+ } else if (m !== 0) {
229
244
  return false;
230
245
  }
246
+ // m === 0: a node of enum or const alone; the checks below decide.
231
247
  if (c.hasEnum && !c.enumSet.has(v)) return false;
232
248
  if (c.hasConst && v !== c.constVal) return false;
233
249
  }
234
250
  if ((seenMask & reqMask) !== reqMask) return false;
235
- if (!plan.additionalAllowed) {
236
- const known = plan.knownKeys;
251
+ if (!P_additionalAllowed) {
252
+ const known = P_knownKeys;
237
253
  for (const k in data) {
238
254
  if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
239
255
  if (!known.has(k)) return false;
240
256
  }
241
257
  }
242
258
  return true;
259
+ };
260
+ }
261
+ function tier0ValidateObject(plan, data) {
262
+ let f = plan._objectVerdict;
263
+ if (f === undefined) f = plan._objectVerdict = makeObjectVerdict(plan);
264
+ return f(data);
265
+ }
266
+ // The verdict for a plan as one closure, for a validator to bind directly.
267
+ function tier0Verdict(plan) {
268
+ if (plan.isPrimitive) { const c = plan.constraints[0]; return (data) => checkPrimitive(c, data); }
269
+ let f = plan._objectVerdict;
270
+ if (f === undefined) f = plan._objectVerdict = makeObjectVerdict(plan);
271
+ return f;
243
272
  }
244
273
 
245
274
  function tier0Validate(plan, data) {
@@ -250,6 +279,7 @@ function tier0Validate(plan, data) {
250
279
  module.exports = {
251
280
  buildTier0Plan,
252
281
  tier0Validate,
282
+ tier0Verdict,
253
283
  tier0ValidateObject,
254
284
  checkPrimitive,
255
285
  TYPE_MASK,
@@ -6,8 +6,14 @@
6
6
  // threshold, version(). Loading it at require cost about 1.9 ms of a 9.7 ms
7
7
  // serverless start, for every process that never calls any of those.
8
8
  let _nativeMod;
9
+ // A module whose exports are one function, as a function. A CDN that turns
10
+ // each CommonJS file into an ES module (esm.sh) hands require() an object
11
+ // with the function on `default`, and calling it threw on the first
12
+ // validator. Node and bundlers hand over the function itself.
13
+ function callable(m) { return typeof m === 'function' ? m : m.default; }
14
+
9
15
  function getNative() {
10
- if (_nativeMod === undefined) _nativeMod = require("./native-load")();
16
+ if (_nativeMod === undefined) _nativeMod = callable(require("./native-load"))();
11
17
  return _nativeMod;
12
18
  }
13
19
  const { env } = require('./env');
@@ -83,9 +89,9 @@ const _hasOwn = Object.prototype.hasOwnProperty;
83
89
 
84
90
  // Build a function that coerces property values to match schema types in-place.
85
91
  // Handles string→number, string→integer, string→boolean, number→string, boolean→string.
86
- function buildCoercer(schema) {
92
+ function buildCoercer(schema, arrayMode) {
87
93
  if (typeof schema !== "object" || schema === null) return null;
88
- const node = buildNodeCoercer(schema, new Set());
94
+ const node = buildNodeCoercer(schema, new Set(), arrayMode === true);
89
95
  if (node === null) return null;
90
96
  return (data) => { if (typeof data === "object" && data !== null) node(data); };
91
97
  }
@@ -101,7 +107,7 @@ function coercionFor(schema, seen) {
101
107
 
102
108
  // The steps for the object or array at `schema`. `seen` stops a schema that
103
109
  // contains itself through an inline cycle.
104
- function buildNodeCoercer(schema, seen) {
110
+ function buildNodeCoercer(schema, seen, arrayMode) {
105
111
  if (seen.has(schema)) return null;
106
112
  seen.add(schema);
107
113
  const steps = [];
@@ -110,13 +116,17 @@ function buildNodeCoercer(schema, seen) {
110
116
  // Assignment to `__proto__` would replace the prototype; the generated
111
117
  // pass skips it for the same reason.
112
118
  if (key === "__proto__") continue;
119
+ // coerceTypes: 'array' wraps a lone value of a top-level property typed
120
+ // array, as the generated pass does; this pass used to leave it.
121
+ const wrap = arrayMode === true && prop && prop.type === "array";
113
122
  const c = coercionFor(prop, seen);
114
- if (!c) continue;
115
- const { scalar, inner } = c;
123
+ if (!c && !wrap) continue;
124
+ const { scalar, inner } = c || {};
116
125
  steps.push((o) => {
117
126
  // Own properties only: `key in o` found values the object inherits,
118
127
  // and the pass then rewrote the shared object behind them.
119
128
  if (!_hasOwn.call(o, key)) return;
129
+ if (wrap && o[key] !== undefined && !Array.isArray(o[key])) o[key] = [o[key]];
120
130
  if (scalar) {
121
131
  const v = scalar(o[key]);
122
132
  if (v !== undefined) o[key] = v;
@@ -964,6 +974,14 @@ class Validator {
964
974
 
965
975
  _ensureCompiled() {
966
976
  if (this._initialized) return;
977
+ // A compile that throws (a custom keyword's compile returning no
978
+ // function, say) leaves the validator uncompiled, so the next call throws
979
+ // the same error. The flag stayed set, and the next call's lazy method
980
+ // called itself until the stack ran out.
981
+ try { this._compile(); } catch (e) { this._initialized = false; throw e; }
982
+ }
983
+
984
+ _compile() {
967
985
  if (LazyRejection === undefined) _loadRejections();
968
986
  this._ensureVocabularies();
969
987
  this._initialized = true;
@@ -1002,7 +1020,7 @@ class Validator {
1002
1020
  // The generated code takes the form of the target engine (js-compiler's
1003
1021
  // setTargetEngine), so a function written for one engine is never handed
1004
1022
  // to a compile asked for the other in the same process.
1005
- const mapKey = compileCacheKey(this._schemaStr, this._schemaMap) + '\u0000' + (_codegen && _codegen.jsCompiler && typeof _codegen.jsCompiler.targetEngine === 'function' ? _codegen.jsCompiler.targetEngine() : 'v8');
1023
+ const mapKey = (_codegen !== null && _codegen.keyFor !== undefined && _codegen.keyFor(this)) || compileCacheKey(this._schemaStr, this._schemaMap) + '\u0000' + (_codegen && _codegen.jsCompiler && typeof _codegen.jsCompiler.targetEngine === 'function' ? _codegen.jsCompiler.targetEngine() : 'v8');
1006
1024
  var _forceNapi = this._interpretOnly || env('ATA_FORCE_NAPI');
1007
1025
  // Custom formats are JS functions: bypass the compile cache since they can
1008
1026
  // differ between validators that share the same schema string. An
@@ -1112,7 +1130,7 @@ class Validator {
1112
1130
  if (!preprocess) {
1113
1131
  // Without a `default` in the text the defaults module stays unloaded.
1114
1132
  const applyDefaults = options.useDefaults === false || !mayHaveDefaults ? null : buildDefaultsApplier(preprocessSchema);
1115
- const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
1133
+ const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema, options.coerceTypes === 'array') : null;
1116
1134
  const applyRemove = options.removeAdditional
1117
1135
  ? buildRemover(preprocessSchema)
1118
1136
  : null;
@@ -1626,7 +1644,7 @@ class Validator {
1626
1644
  _ensureNative() {
1627
1645
  if (this._nativeReady) return;
1628
1646
  this._nativeReady = true;
1629
- require('./native-attach.js')(this, getNative());
1647
+ callable(require('./native-attach.js'))(this, getNative());
1630
1648
  }
1631
1649
 
1632
1650
  // The buffer APIs, installed on the first call to any of them. The schema
@@ -1647,12 +1665,17 @@ class Validator {
1647
1665
  if (!native && typeof Buffer === 'undefined') throw new Error(`Native addon required for ${name}(). Use validate(), isValidObject() or validateJSON(), which do not need it.`);
1648
1666
  const { bufferNeedsSlowPath, installSlowBufferApis } = require('./buffer-gate.js');
1649
1667
  if (!native) return installSlowBufferApis(this);
1650
- require('./native-attach.js').installFast(this, native, bufferNeedsSlowPath, installSlowBufferApis);
1668
+ callable(require('./native-attach.js')).installFast(this, native, bufferNeedsSlowPath, installSlowBufferApis);
1651
1669
  }
1652
1670
 
1653
1671
  addSchema(schema) {
1654
- if (this._initialized) {
1655
- throw new Error('Cannot add schema after compilation — call addSchema() before validate()')
1672
+ // After a compile of either kind: the verdict-only one isValidObject()
1673
+ // makes (_jsFn) binds a function over the registry as it was, and a
1674
+ // schema added after it reached validate() but not isValidObject(),
1675
+ // which kept answering against the old definition. A call the
1676
+ // interpreted twin answered compiled nothing, and the twin is rebuilt.
1677
+ if (this._initialized || this._jsFn) {
1678
+ throw new Error('Cannot add schema after compilation: call addSchema() before validate(), isValidObject(), validateJSON() or isValidJSON()')
1656
1679
  }
1657
1680
  if (!schema || !schema.$id) {
1658
1681
  throw new Error('Schema must have $id')
@@ -1688,7 +1711,12 @@ class Validator {
1688
1711
  return;
1689
1712
  }
1690
1713
  this._ensureVocabularies();
1691
- if (this._interpretOnly || env('ATA_FORCE_NAPI') || _loadCodegen() === null) return;
1714
+ // Where the realm refuses generated code (a strict CSP,
1715
+ // --disallow-code-generation-from-strings) the verdict-only compile could
1716
+ // only reach the closure compiler, which answers in about twice the time
1717
+ // of the interpreted engine the full compile binds there; the callers
1718
+ // fall through to that compile when this leaves no function.
1719
+ if (this._interpretOnly || env('ATA_FORCE_NAPI') || _loadCodegen() === null || !codegenAvailable()) return;
1692
1720
  _codegen.compileVerdict.call(this);
1693
1721
  }
1694
1722
 
@@ -1710,7 +1738,7 @@ class Validator {
1710
1738
 
1711
1739
  // Mutators
1712
1740
  const applyDefaults = buildDefaultsApplier(schemaObj);
1713
- const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
1741
+ const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj, options.coerceTypes === 'array') : null;
1714
1742
  const applyRemove = options.removeAdditional
1715
1743
  ? buildRemover(schemaObj)
1716
1744
  : null;
@@ -1941,22 +1969,6 @@ function compile(schema, opts) {
1941
1969
  // and the suggestion helper are exported by index.js. None of them is called
1942
1970
  // by a Validator, so ata-validator/lite leaves them out.
1943
1971
 
1944
- // Authoring helper: identity at runtime. Its only job is to attach the
1945
- // JSONSchema type (see index.d.ts) to an inline schema object so TypeScript
1946
- // gives autocomplete and value checking while authoring. Returns the schema
1947
- // untouched so it can be passed straight to Validator, toStandaloneModule, etc.
1948
- // `schemaOf<T>()`: the JSON Schema of a TypeScript type, written in by
1949
- // @ata-project/unplugin at build time from the type and its JSDoc tags. It
1950
- // has no run-time form: a call that reaches here was not replaced, and the
1951
- // loud error is better than an empty schema that accepts everything.
1952
- function schemaOf () {
1953
- throw new Error('schemaOf<T>() is replaced at build time by @ata-project/unplugin (with ts-json-schema-generator installed); it has no run-time form. Write the JSON Schema, or build with the plugin.');
1954
- }
1955
-
1956
- function defineSchema (schema) {
1957
- return schema;
1958
- }
1959
-
1960
1972
  // Public methods start as memoized accessors on the prototype. A fresh
1961
1973
  // Validator allocates none of them; the first read of a method builds the
1962
1974
  // bound closure, stores it on the instance as an ordinary writable property
@@ -2349,16 +2361,22 @@ function _ticks(self) {
2349
2361
  // _ticks says so.
2350
2362
  function _tier0Verdict(self) {
2351
2363
  if (require('./shape-classifier').classify(self._schemaObj).tier !== 0) return null;
2352
- const { buildTier0Plan, tier0Validate } = require('./tier0');
2364
+ const { buildTier0Plan, tier0Verdict } = require('./tier0');
2353
2365
  const plan = buildTier0Plan(self._schemaObj, self._userFormats);
2354
- if (!_ticks(self)) return (d) => tier0Validate(plan, d);
2366
+ const verdict = tier0Verdict(plan);
2367
+ if (!_ticks(self)) return verdict;
2355
2368
  const tick = _tiering.counter(self);
2356
- return (d) => { tick(); return tier0Validate(plan, d); };
2369
+ return (d) => { tick(); return verdict(d); };
2357
2370
  }
2358
2371
 
2359
2372
  Validator.prototype._coldTwin = function () {
2360
2373
  const cg = _loadCodegen();
2361
- return cg === null || cg.coldTwin === undefined ? null : cg.coldTwin(this, Validator);
2374
+ // Where no code can be generated (a strict CSP), the interpreted engine is
2375
+ // the compile itself: a twin would only delay it, and its size test read
2376
+ // the schema's text for nothing (6.5% of a schema's first check there).
2377
+ // The probe of the realm (codegenAvailable) is not run for a validator
2378
+ // asked for the interpreted engine, which never generates code.
2379
+ return cg === null || cg.coldTwin === undefined || this._interpretOnly || !codegenAvailable() ? null : cg.coldTwin(this, Validator);
2362
2380
  };
2363
2381
 
2364
2382
  _defineLazyMethod('validate', (self) => (data) => {
@@ -2395,7 +2413,14 @@ _defineLazyMethod('isValidObject', (self) => {
2395
2413
  _bindVerdict(self, _ticks(self) ? _t0 : (d) => {
2396
2414
  const r = _t0(d);
2397
2415
  if (++_n === 2) {
2416
+ const before = self.isValidObject;
2398
2417
  try { self._ensureCodegen(); } catch {}
2418
+ // Where the generated code did not take the method over (no code
2419
+ // generation in this realm, or the generator declined), the plan's
2420
+ // closure answers directly from now on: this wrapper cost every
2421
+ // later call a count and a call, 13.5 against 4.7 ns on an empty
2422
+ // object, on exactly the pages and workers that have no generator.
2423
+ if (self.isValidObject === before) _bindVerdict(self, _t0);
2399
2424
  }
2400
2425
  return r;
2401
2426
  });
@@ -2457,8 +2482,6 @@ module.exports = {
2457
2482
  createPaddedBuffer,
2458
2483
  SIMDJSON_PADDING,
2459
2484
  parseJSON,
2460
- defineSchema,
2461
- schemaOf,
2462
2485
  };
2463
2486
  // How an entry registers the code generator. Not enumerable, so the exported
2464
2487
  // surface of `require('ata-validator')` is what it was.