ata-validator 1.42.0 → 1.44.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 CHANGED
@@ -244,21 +244,21 @@ is the most common way to get a misleading number out of this library.
244
244
 
245
245
  | | compiled with `ata build` | runtime `new Validator(schema)` |
246
246
  |---|---|---|
247
- | In a bundle, gzipped | **2.2 KB** | 95.6 KB |
248
- | Time to a served request | **3.5 ms** | 7.8 ms |
247
+ | In a bundle, gzipped | **2.1 KB** | 110.7 KB |
248
+ | Time to a served request | **3.5 ms** | 8.2 ms |
249
249
  | Schema known when | build time | any time |
250
250
 
251
251
  The bundle row is the ten-field user schema in
252
252
  `tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
253
- `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.36.0.
254
- The startup row is a Hono route on Bun 1.4, median of nine interleaved runs, from
255
- `benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
253
+ `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.43.0.
254
+ The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
255
+ `benchmark/bundle`, against 3.4 ms for the same app doing no validation at all, so the
256
256
  compiled path costs nothing measurable to start. The runtime figure is what it is because
257
257
  a schema that arrives at run time can use any keyword, so the whole engine has to be
258
258
  there. The compiled module imports nothing and contains only the checks your schema asks
259
259
  for.
260
260
 
261
- **On a server, use whichever fits your schemas.** 96 KB of JavaScript on a Node or Bun
261
+ **On a server, use whichever fits your schemas.** 111 KB of JavaScript on a Node or Bun
262
262
  process is not a cost anyone notices, and the runtime API is the simpler thing to reach
263
263
  for. Speed is the same either way once warm.
264
264
 
@@ -491,7 +491,7 @@ const v = new Validator(schema, {
491
491
 
492
492
  ### Build-time compile (`ata compile`)
493
493
 
494
- The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.2 KB gzipped, full error detail included, against 95.6 KB for the runtime bundled for the browser.
494
+ The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 110.7 KB for the runtime bundled for the browser (ata 1.43.0).
495
495
 
496
496
  ```bash
497
497
  npx ata compile schemas/user.json -o src/generated/user.validator.mjs
@@ -533,13 +533,13 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
533
533
  Run with `--watch` during development for incremental rebuilds.
534
534
 
535
535
  Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
536
- minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.36.0:
536
+ minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.43.0:
537
537
 
538
538
  | What the app imports | Size | Notes |
539
539
  |---|---|---|
540
- | `Validator` from `ata-validator` | 95.6 KB | The compiler ships with it, because a runtime schema can use any keyword |
540
+ | `Validator` from `ata-validator` | 110.7 KB | The compiler ships with it, because a runtime schema can use any keyword |
541
541
  | `isValid` from the compiled module | **1.3 KB** | Nothing else is reachable, so the error collector is dropped |
542
- | `validate` from the compiled module | **2.0 KB** | Adds the detailed error collector |
542
+ | `validate` from the compiled module | **1.9 KB** | Adds the detailed error collector |
543
543
 
544
544
  `--abort-early` makes the generated source about three times smaller, and after
545
545
  bundling it makes no difference: importing only `isValid` already leaves the error
package/index.js CHANGED
@@ -434,6 +434,9 @@ function installCodegenPaths (ctx) {
434
434
  }
435
435
  return nativeText ? getNative().rawFastValidate(this._fastSlot, Buffer.from(jsonStr)) : undefined;
436
436
  };
437
+ // The parsed document is left on `_lastParsed` for the rich layer around
438
+ // validateJSON, which hands it to the rejection so reading the errors does
439
+ // not parse the text again (see LazyJsonRejection).
437
440
  const validateText = (jsonStr) => {
438
441
  let obj;
439
442
  try {
@@ -442,6 +445,7 @@ function installCodegenPaths (ctx) {
442
445
  if (!(e instanceof SyntaxError)) throw e;
443
446
  return _jsonSyntaxRejection(e);
444
447
  }
448
+ this._lastParsed = obj;
445
449
  return jsonValidateFn(obj);
446
450
  };
447
451
  this.validateJSON = useSimdjsonForLarge && !preprocess
@@ -503,7 +507,7 @@ function installCodegenPaths (ctx) {
503
507
  // The verdict function alone, for isValidObject() before a full compile: the
504
508
  // error and combined generators are left for the first rejection.
505
509
  installCodegenPaths.compileVerdict = function compileVerdict () {
506
- const { compileCacheKey, _compileCache, _bindVerdict, _rememberInstance } = core._internals;
510
+ const { compileCacheKey, _compileCache, _compileCacheSet, _bindVerdict, _rememberInstance } = core._internals;
507
511
  if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
508
512
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
509
513
  const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
@@ -532,7 +536,7 @@ installCodegenPaths.compileVerdict = function compileVerdict () {
532
536
  // validator that later reuses this entry reports the same engine it
533
537
  // would have compiled to.
534
538
  if (!uf && !this._keywords) {
535
- if (!cached) _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
539
+ if (!cached) _compileCacheSet(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
536
540
  else cached.jsFn = jsFn;
537
541
  }
538
542
  }
@@ -878,8 +882,58 @@ function buildParse (self, decline, extended) {
878
882
  // preprocess pass, the JSON-text scanner, the parse() copy and the
879
883
  // ahead-of-time bundle methods. parse() and the bundle methods load on first
880
884
  // use.
885
+ // Cold start. A validator's first COLD_CALLS calls are answered by an
886
+ // interpreted twin, built from the same schema and options with
887
+ // `engine: 'interpreter'`, which the test suite holds to the same answers and
888
+ // errors as the generated code. Generating code and having V8 compile it is
889
+ // the largest part of a first call on a large schema: from process start to
890
+ // one rejection with its error read, SchemaStore's SARIF schema took 87 ms
891
+ // with code generated at the first call and 31 ms with the twin answering.
892
+ // A validator still in use after that compiles as before, so a server reaches
893
+ // the generated code's speed within its first requests, and a command line
894
+ // tool or a short-lived function never pays to compile. Only where compiling
895
+ // costs enough to matter: a schema whose size with its references followed
896
+ // passes COLD_MIN characters (expandedChars in lib/js-compiler.js). A small
897
+ // schema compiles in about a millisecond and is generated at its first call
898
+ // as before, which also keeps every test that compares the generated code
899
+ // with the interpreter comparing the generated code. Not for a validator with
900
+ // an extension (_extendValidate, _extendChecks), which the twin would not
901
+ // carry. Returns the twin to answer this call, or null to compile.
902
+ const COLD_CALLS = 64;
903
+ const COLD_MIN = 16 * 1024;
904
+ // Whether the schema may expand past `limit`, answered from its text where
905
+ // that settles it. Without a `$ref` nothing expands, and expandedChars counts
906
+ // at most 8 for each character of the text, so a short schema is decided
907
+ // without walking it. The text is the one _ensureCompiled needs anyway. Walking
908
+ // every small route schema here cost a Fastify boot of ten routes 0.15 ms.
909
+ function mayExpandPast (self, limit) {
910
+ const str = self._schemaStr || (self._schemaStr = JSON.stringify(self._schemaObj));
911
+ if (str === undefined) return false;
912
+ if (str.length * 8 <= limit && !str.includes('"$ref"')) return false;
913
+ return jsCompiler.expandedChars(self._schemaObj, limit) > limit;
914
+ }
915
+
916
+ function coldTwin (self, Validator) {
917
+ const n = self._coldCalls === undefined ? 0 : self._coldCalls;
918
+ if (n >= COLD_CALLS || self._validateTail !== null || self._verdictTail !== null) return null;
919
+ let t = self._twin;
920
+ if (t === undefined) {
921
+ t = null;
922
+ if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
923
+ try { t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' })); } catch { t = null; }
924
+ }
925
+ self._twin = t;
926
+ }
927
+ if (t === null) return null;
928
+ self._coldCalls = n + 1;
929
+ return t;
930
+ }
931
+
881
932
  core._registerCodegen({
882
933
  jsCompiler,
934
+ coldTwin,
935
+ // The native locator for error frames, loaded with the first frame.
936
+ nativePositions: () => require('./lib/native-positions').nativeTargeted,
883
937
  installPaths: installCodegenPaths,
884
938
  compileVerdict: installCodegenPaths.compileVerdict,
885
939
  installScanner: installCodegenPaths.installScanner,
@@ -44,6 +44,21 @@ function __ataScore (errs) {
44
44
  // `title`, or the titles in `titles` when given (the generated code passes
45
45
  // each branch's result as it is rather than copying it to add one).
46
46
  // Returns the single error to report, or null when the keyword is satisfied.
47
+ // oneOf passed by more than one branch; generated code builds it from a count.
48
+ function __ataMulti (passIdx, total, pp, sp, o) {
49
+ const multi = {
50
+ code: 'ATA4002',
51
+ keyword: 'oneOf',
52
+ instancePath: pp || '',
53
+ path: pp || '',
54
+ schemaPath: sp,
55
+ message: 'value matched ' + passIdx.length + ' of ' + total + ' oneOf variants, expected exactly one',
56
+ params: { passingSchemas: passIdx },
57
+ };
58
+ if (o !== null) Object.defineProperty(multi, '_o', { value: o });
59
+ return multi;
60
+ }
61
+
47
62
  function __ataCollapse (kw, br, pp, sp, o, titles) {
48
63
  let passN = 0;
49
64
  for (let i = 0; i < br.length; i++) if (br[i].valid) passN++;
@@ -53,17 +68,7 @@ function __ataCollapse (kw, br, pp, sp, o, titles) {
53
68
  if (pass.length > 1) {
54
69
  const passIdx = [];
55
70
  for (let i = 0; i < br.length; i++) if (br[i].valid) passIdx.push(i);
56
- const multi = {
57
- code: 'ATA4002',
58
- keyword: 'oneOf',
59
- instancePath: pp || '',
60
- path: pp || '',
61
- schemaPath: sp,
62
- message: 'value matched ' + pass.length + ' of ' + br.length + ' oneOf variants, expected exactly one',
63
- params: { passingSchemas: passIdx },
64
- };
65
- if (o !== null) Object.defineProperty(multi, '_o', { value: o });
66
- return multi;
71
+ return __ataMulti(passIdx, br.length, pp, sp, o);
67
72
  }
68
73
  } else if (pass.length >= 1) {
69
74
  return null;
@@ -97,12 +102,13 @@ function collapseBranches ({ keyword, branchResults, parentPath, parentSchemaPat
97
102
  return __ataCollapse(keyword, branchResults, parentPath, parentSchemaPath, ordinal === undefined ? null : ordinal);
98
103
  }
99
104
 
100
- // The three declarations as source, for the generated code to carry. Derived
105
+ // The four declarations as source, for the generated code to carry. Derived
101
106
  // from the functions above rather than written out again, so a fix to one
102
107
  // cannot miss the other. `tests/test_branch_collapse.js` runs both.
103
108
  function embedSource () {
104
109
  return 'const __ATA_SEVERITY=' + JSON.stringify(__ATA_SEVERITY) + ';' +
105
110
  'const __ataScore=' + __ataScore.toString() + ';' +
111
+ 'const __ataMulti=' + __ataMulti.toString() + ';' +
106
112
  'const __ataCollapse=' + __ataCollapse.toString() + ';';
107
113
  }
108
114
 
package/lib/clone-emit.js CHANGED
@@ -78,8 +78,11 @@ function inlineRefsForClone(root) {
78
78
  for (const k of Object.keys(cur)) {
79
79
  if (strip && k === 'default') continue;
80
80
  const v = cur[k];
81
- if (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') { out[k] = v; continue; }
82
- out[k] = walk(v, active, strip);
81
+ // A property named "__proto__" is an own key of the copy: assigned, it
82
+ // set the copy's prototype and the property was gone from parse().
83
+ const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active, strip);
84
+ if (k === '__proto__') Object.defineProperty(out, k, { value: w, writable: true, enumerable: true, configurable: true });
85
+ else out[k] = w;
83
86
  }
84
87
  if (siblingDefault !== undefined) out.default = siblingDefault;
85
88
  return out;
@@ -2,13 +2,17 @@
2
2
 
3
3
  const { buildDataPositionMap, buildTargetedPositionMap } = require('./data-positions');
4
4
 
5
+
5
6
  /**
6
7
  * Memoize the position map for the duration of a single validate() call.
7
8
  * Caller passes the original buffer/string. Identity-keyed: same reference
8
9
  * == same map. No global state, caller holds the cache instance.
9
10
  */
10
11
 
11
- function createCache () {
12
+ // `nativeTargeted`, when given, is a faster source of the same entries: the
13
+ // full entry point passes lib/native-positions.js, lite passes nothing.
14
+ function createCache (nativeTargeted) {
15
+ const _nativeTargeted = typeof nativeTargeted === 'function' ? nativeTargeted : null;
12
16
  const wm = new WeakMap();
13
17
  const sm = new Map(); // strings can't go in WeakMap; clear after each validate
14
18
  let lastInput = null;
@@ -32,11 +36,13 @@ function createCache () {
32
36
  ? sm.get(input)
33
37
  : (Buffer.isBuffer(input) ? wm.get(input) : undefined);
34
38
  if (already) return already;
35
- let filtered;
36
- try {
37
- filtered = buildTargetedPositionMap(input, wanted);
38
- } catch {
39
- return null;
39
+ let filtered = _nativeTargeted === null ? null : _nativeTargeted(input, wanted);
40
+ if (filtered === null) {
41
+ try {
42
+ filtered = buildTargetedPositionMap(input, wanted);
43
+ } catch {
44
+ return null;
45
+ }
40
46
  }
41
47
  let full;
42
48
  const fallback = (key) => {
package/lib/draft7.js CHANGED
@@ -49,13 +49,11 @@ function _normalize(schema) {
49
49
  // dependencies → dependentSchemas + dependentRequired
50
50
  if (schema.dependencies) {
51
51
  for (const [key, value] of Object.entries(schema.dependencies)) {
52
- if (Array.isArray(value)) {
53
- if (!schema.dependentRequired) schema.dependentRequired = {}
54
- schema.dependentRequired[key] = value
55
- } else {
56
- if (!schema.dependentSchemas) schema.dependentSchemas = {}
57
- schema.dependentSchemas[key] = value
58
- }
52
+ // defineProperty: a dependency on a property named "__proto__" is an
53
+ // own key here, where assignment would set the map's prototype.
54
+ const target = Array.isArray(value) ? 'dependentRequired' : 'dependentSchemas'
55
+ if (!schema[target]) schema[target] = {}
56
+ Object.defineProperty(schema[target], key, { value, writable: true, enumerable: true, configurable: true })
59
57
  }
60
58
  delete schema.dependencies
61
59
  }
@@ -30,7 +30,7 @@ function errorSite (keyword, schemaPath, params, format) {
30
30
  };
31
31
  }
32
32
 
33
- function makeRich (site, path, params, message, value, root) {
33
+ function makeRich (site, path, params, message, value, root, spPrefix) {
34
34
  // As resolveAt: a root that is null, undefined, '' or NaN resolves nothing.
35
35
  const at = (!root && root !== 0 && root !== false) ? MISSING : value;
36
36
  const out = {
@@ -40,7 +40,7 @@ function makeRich (site, path, params, message, value, root) {
40
40
  path,
41
41
  expected: site.expected === NO_EXPECTED ? expectedFor({ keyword: site.keyword, params }) : site.expected,
42
42
  received: at === MISSING ? undefined : reprValue(at),
43
- schemaPath: site.schemaPath,
43
+ schemaPath: spPrefix === undefined ? site.schemaPath : spPrefix + site.schemaPath,
44
44
  docUrl: site.docUrl,
45
45
  instancePath: path,
46
46
  dataPath: path,