ata-validator 1.7.0 → 1.7.2
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/CHANGELOG.md +29 -0
- package/README.md +13 -3
- package/index.d.ts +3 -3
- package/index.js +141 -47
- package/lib/aot.js +1 -1
- package/lib/interpreter.js +251 -45
- package/lib/js-compiler.js +99 -44
- package/lib/plan-compiler.js +969 -0
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 1.7.2 - 2026-08-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `enum` compared object members by their serialized form, so `{"b": 2, "a": 1}` was rejected against `enum: [{"a": 1, "b": 2}]`. JSON Schema compares instances by value: two objects with the same members are equal whatever order their keys were written in. All three code generation paths did this, so they agreed with each other and disagreed with the interpreted engine, which has always compared structurally. `const` was already correct but reached the answer by building a canonical string on every call. Both now use one structural comparison hoisted per compiled function. The official suite does not cover reordered keys for `enum`, and the entry-point agreement test compares the generators against each other, so neither would have caught this; `tests/test_value_equality.js` pins the answer through every engine that can produce one and runs as part of `npm test`.
|
|
10
|
+
- A draft-07 document passed through the `schemas` option was rejected with "Schema in schemas option must have $id" whenever it carried `$ref` next to `$id`. Draft-07 ignores every keyword sitting beside `$ref`, so normalization drops them and `$id` goes with them, which is the right reading for evaluation: the reference resolves against the URI the document was retrieved from, not the `$id` it declares. It is the wrong reading for registration, where `$id` is the only name the caller gave the document. The identity is now read from what the caller passed when normalization has dropped it, so such a document registers under its `$id` in the array form and is addressable by both its retrieval URI and its `$id` in the map form. A document that declares no `$id` at all is still refused, and a fragment-only `$id` is still an anchor rather than a document name. This is what made all 23 remote-reference cases of the draft-07 suite error out in the Bowtie compliance report; they now answer, and answer correctly.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The closure-tree compiler now covers every schema the interpreted engine accepts. It previously declined `unevaluatedProperties`/`unevaluatedItems` and `$dynamicRef` in any schema with more than one resource, which left 192 of the 987 suite schemas walking the generic evaluator; that count is now zero. Annotations flow through the compiled tree: the compiler decides at compile time which nodes anyone reads annotations from, and those nodes carry a record that in-place children write into directly, with a failed child rolled back by truncating the record rather than by allocating a fresh one per child. `$dynamicRef` resolves against the dynamic scope computed at compile time, so multi-resource schemas no longer search it per call. A schema whose compiled graph has no cycle drops the recursion guard and the `(schema, data)` stack entirely. An unresolvable `$ref` compiles into the same runtime rejection the evaluator produces instead of declining the schema.
|
|
15
|
+
- Measured on the official suite with prebuilt validators, several runs each on the same machine: draft 2020-12 went from 94 ns to 77-80 ns per case overall, and the cases that route to the interpreted engine from about 240 ns to about 125 ns when they accept and 235 ns to 160 ns when they reject. Draft 7 went from 59 ns to 49 ns overall. The metaschema-validation case, the worst outlier in the suite, went from 5.5 µs to about 1.4 µs. On a longer steady-state loop over the same suite, where construction is not part of what is timed, draft 2020-12 went from 46 ns to 43 ns per case and draft 7 from 33 ns to 29 ns.
|
|
16
|
+
- The value-level keywords of a schema node are compiled into closures over their own operands instead of being answered by a generic evaluator that re-reads a dozen presence flags off the plan on every call. The type of the value is computed once per node rather than once per keyword group, and a node whose keywords never need it does not compute it at all.
|
|
17
|
+
- `uniqueItems` on arrays whose item type is not known ahead of time no longer builds a canonicalizing closure and a `Set` on every call. The helpers are hoisted once per compiled function; arrays of twelve items or fewer compare pairwise with a structural equality that ignores key order and allocates nothing, and longer arrays keep the `Set`, using canonical strings only once an object actually appears. Over the draft 7 suite the `uniqueItems` cases went from 125 ns to 24 ns each.
|
|
18
|
+
- `ARCHITECTURE.md` documents the interpreted engine and the closure-tree compiler, and no longer claims `$dynamicRef` falls back to the native engine, which stopped being true in 1.7.1.
|
|
19
|
+
|
|
20
|
+
## 1.7.1 - 2026-08-23
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- A closure-tree compiler for the interpreted engine. A schema the code generator declines is compiled into a tree of plain closures, one per schema node, with every keyword branch decided at compile time and `$ref` targets resolved once; no source generation and no `new Function`, so it works under a CSP and on Workers. Scope: schemas without `unevaluatedProperties`/`unevaluatedItems`, and `$dynamicRef` only in single-resource schemas; everything else keeps the generic evaluator. `tests/test_plan_compiler.js` holds the compiled tree to byte-identical verdicts and errors against the evaluator over 2,864 suite cases.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Plans that check only value-level keywords (most leaves of any schema) skip the evaluator's prologue entirely; `$ref` resolutions are cached with their planned target on the plan itself; the dynamic scope is pushed and popped in place instead of copied per resource. Rejection results are a small class with the `errors` accessor on the prototype, since defining a getter inside an object literal builds a closure and an accessor property on every rejection, which was the single largest cost on the rejection path.
|
|
29
|
+
- The interpreted engine gained a verdict-only mode: `isValidObject()` and the internal fast checks walk the schema without constructing a single error object, message string or scratch array. On an interpreter-routed schema the boolean check dropped from 894 ns to 70 ns.
|
|
30
|
+
- Object validation for `$dynamicRef` schemas no longer routes to the native engine. The interpreted engine has scored the same on every `$dynamicRef` case of the suite since the dynamic-scope fix in 1.7.0, needs no addon, and carries the verdict-only mode; the suite's `$dynamicRef` rejects dropped from about 1.7 µs to the interpreter's cost.
|
|
31
|
+
- String length bounds decide from the UTF-16 length where possible: a string's code point count always sits between half its length and its length, so `minLength`/`maxLength` only count code points inside the narrow band where the answer is genuinely uncertain. The surrogate test is a single wraparound compare. The code generator already worked this way; the interpreter and the closure path now match it.
|
|
32
|
+
- Errors are paid for when read, not when produced. `validate()` answers the verdict from the fastest engine for the schema and materializes `errors` through a cached getter on first access; declaration-order sorting and enrichment (received value, suggestions, source frames) moved with it into one presentation layer. A caller that only reads `.valid`, which is every gateway check, no longer pays for error construction at all. The output of `.errors` is byte-for-byte what it was. Measured on a suite-shaped benchmark of prebuilt validators over 1,052 mixed valid and invalid cases, `validate().valid` went from 778 ns to about 150 ns per call, ahead of every error-capable validator we measured, and a rejection that never has its errors read now costs less than `abortEarly` mode used to. One observable edge: mutating the data between `validate()` and the first read of `.errors` now reflects the mutated data in the errors, and if the mutation makes the data valid the errors fall back to a single generic entry.
|
|
33
|
+
|
|
5
34
|
## 1.7.0 - 2026-08-23
|
|
6
35
|
|
|
7
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -64,9 +64,10 @@ passes, and a rejection costs more than a verdict: ata builds an error carrying
|
|
|
64
64
|
offending value, a documentation link and a suggestion. On a five-field object schema a
|
|
65
65
|
passing payload costs about 17 ns and a rejected one about 155 ns. `abortEarly: true` or
|
|
66
66
|
`isValidObject()` skips that work when only the verdict matters. Schemas ata declines to
|
|
67
|
-
compile, mostly cross-document `$ref`, `$dynamicRef` and `unevaluated
|
|
68
|
-
interpreted engine
|
|
69
|
-
|
|
67
|
+
compile, mostly cross-document `$ref`, `$dynamicRef` and the harder `unevaluated*` shapes,
|
|
68
|
+
run on the interpreted engine. That engine compiles each schema into a tree of closures, so
|
|
69
|
+
on a six-field object schema a passing payload costs about 136 ns against 8 ns for the same
|
|
70
|
+
shape on the compiled path, both measured warm in one run on the same machine.
|
|
70
71
|
|
|
71
72
|
## Error messages
|
|
72
73
|
|
|
@@ -169,6 +170,9 @@ v.isValidJSON('{"name": "Mert", "email": "mert@example.com"}'); // true
|
|
|
169
170
|
// Buffer input (zero-copy, raw NAPI)
|
|
170
171
|
v.isValid(Buffer.from('{"name": "Mert", "email": "mert@example.com"}'));
|
|
171
172
|
|
|
173
|
+
// Which engine answers this schema: 'codegen', 'closure', 'native' or 'interpreter'
|
|
174
|
+
v.engine(); // 'codegen'
|
|
175
|
+
|
|
172
176
|
// Parallel batch - multi-core, NDJSON, 13.4M items/sec
|
|
173
177
|
const ndjson = Buffer.from(lines.join('\n'));
|
|
174
178
|
v.isValidParallel(ndjson); // bool[]
|
|
@@ -400,6 +404,12 @@ const { toStandaloneModule } = require('ata-validator/build');
|
|
|
400
404
|
fs.writeFileSync('./user.validator.mjs', toStandaloneModule(schema, { format: 'esm' }));
|
|
401
405
|
```
|
|
402
406
|
|
|
407
|
+
Custom format functions either get their source embedded (the default, refused
|
|
408
|
+
at build time with a named error when the function would not survive
|
|
409
|
+
serialization) or, with `formatMode: 'inject'`, are supplied at load time
|
|
410
|
+
through a `setFormats()` export the module carries. `docs/API.md` has the
|
|
411
|
+
details.
|
|
412
|
+
|
|
403
413
|
**Fastify startup, 10 route schemas, from a cold process to the first validated request:
|
|
404
414
|
ajv 19.6 ms, ata 3.1 ms, no build step required.** ata registers in 1.1 ms of that and
|
|
405
415
|
compiles on the first request, so counting only registration would overstate the gap.
|
package/index.d.ts
CHANGED
|
@@ -403,9 +403,9 @@ export interface Validator<T = unknown> {
|
|
|
403
403
|
isValidObject(data: unknown): data is T;
|
|
404
404
|
/**
|
|
405
405
|
* Which engine answers `validate()` for this schema: 'codegen' (generated
|
|
406
|
-
* JS), 'closure' (the closure compiler)
|
|
407
|
-
*
|
|
408
|
-
*
|
|
406
|
+
* JS), 'closure' (the closure compiler) or 'interpreter'; 'native' is
|
|
407
|
+
* reserved. The verdict is the same on every engine; the cost is not.
|
|
408
|
+
* A diagnostic, not a configuration.
|
|
409
409
|
*/
|
|
410
410
|
engine(): 'codegen' | 'closure' | 'native' | 'interpreter';
|
|
411
411
|
|
package/index.js
CHANGED
|
@@ -314,6 +314,34 @@ const ABORT_EARLY_RESULT = Object.freeze({
|
|
|
314
314
|
|
|
315
315
|
// Above this size, simdjson On Demand (selective field access) beats JSON.parse
|
|
316
316
|
// (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
|
|
317
|
+
|
|
318
|
+
// Rejection result with errors materialized on first read. The accessor
|
|
319
|
+
// lives on the prototype so constructing one is a plain allocation; an
|
|
320
|
+
// object-literal getter would create a closure and define an accessor
|
|
321
|
+
// property on every rejection, which showed up as the single largest cost
|
|
322
|
+
// on the rejection path. `toJSON` keeps JSON.stringify output identical to
|
|
323
|
+
// the eager shape. Note for tests: deepStrictEqual against a plain object
|
|
324
|
+
// compares prototypes; read `.errors` and compare that.
|
|
325
|
+
class LazyRejection {
|
|
326
|
+
constructor(build, data) {
|
|
327
|
+
this.valid = false;
|
|
328
|
+
this._build = build;
|
|
329
|
+
this._data = data;
|
|
330
|
+
this._errors = null;
|
|
331
|
+
}
|
|
332
|
+
toJSON() {
|
|
333
|
+
return { valid: false, errors: this.errors };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
Object.defineProperty(LazyRejection.prototype, 'errors', {
|
|
337
|
+
enumerable: true,
|
|
338
|
+
configurable: true,
|
|
339
|
+
get() {
|
|
340
|
+
if (this._errors === null) this._errors = this._build(this._data);
|
|
341
|
+
return this._errors;
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
317
345
|
const SIMDJSON_THRESHOLD = 8192;
|
|
318
346
|
|
|
319
347
|
// Resolve a JSON Schema path like "#/properties/name/type" to the schema object
|
|
@@ -440,6 +468,23 @@ function _normalizeCallerSchema(s, inheritDraft7) {
|
|
|
440
468
|
return JSON.stringify(copy) === str ? s : copy
|
|
441
469
|
}
|
|
442
470
|
|
|
471
|
+
// The identity a document is registered under. Draft-07 ignores every
|
|
472
|
+
// keyword sitting next to `$ref`, so normalization drops them, `$id` among
|
|
473
|
+
// them: that is the right reading for evaluation, where the reference
|
|
474
|
+
// resolves against the retrieval URI rather than the declared `$id`. It is
|
|
475
|
+
// the wrong reading for registration, since `$id` is how the caller names
|
|
476
|
+
// the document. So the identity is read from the normalized copy first and
|
|
477
|
+
// from what the caller passed second. A bare-fragment `$id` is a draft-07
|
|
478
|
+
// anchor rather than a document identity, and normalization has already
|
|
479
|
+
// turned it into `$anchor`, so it is not used here.
|
|
480
|
+
function declaredId(original, normalized) {
|
|
481
|
+
const n = normalized && typeof normalized === 'object' ? normalized.$id : undefined
|
|
482
|
+
if (typeof n === 'string' && n !== '') return n
|
|
483
|
+
const o = original && typeof original === 'object' ? original.$id : undefined
|
|
484
|
+
if (typeof o === 'string' && o !== '' && o[0] !== '#') return o
|
|
485
|
+
return undefined
|
|
486
|
+
}
|
|
487
|
+
|
|
443
488
|
// `inheritDraft7` is true when the root schema is draft-07: a retrieved
|
|
444
489
|
// document that declares no dialect is read under the root's draft.
|
|
445
490
|
function buildSchemaMap(schemas, inheritDraft7) {
|
|
@@ -448,7 +493,7 @@ function buildSchemaMap(schemas, inheritDraft7) {
|
|
|
448
493
|
if (Array.isArray(schemas)) {
|
|
449
494
|
for (const s of schemas) {
|
|
450
495
|
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
451
|
-
const id = normalized
|
|
496
|
+
const id = declaredId(s, normalized)
|
|
452
497
|
if (!id) throw new Error('Schema in schemas option must have $id')
|
|
453
498
|
map.set(id, normalized)
|
|
454
499
|
}
|
|
@@ -459,7 +504,8 @@ function buildSchemaMap(schemas, inheritDraft7) {
|
|
|
459
504
|
// under and by the $id it declares. Registering only the $id makes
|
|
460
505
|
// references to the retrieval URI unresolvable.
|
|
461
506
|
map.set(key, normalized)
|
|
462
|
-
|
|
507
|
+
const id = declaredId(s, normalized)
|
|
508
|
+
if (id && id !== key) map.set(id, normalized)
|
|
463
509
|
}
|
|
464
510
|
}
|
|
465
511
|
return map
|
|
@@ -904,6 +950,10 @@ class Validator {
|
|
|
904
950
|
} catch {}
|
|
905
951
|
}
|
|
906
952
|
|
|
953
|
+
// The boolean engine is the verdict authority for these paths; the
|
|
954
|
+
// final lazy wrapper uses it to skip error construction entirely.
|
|
955
|
+
if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
|
|
956
|
+
|
|
907
957
|
if (options.abortEarly && jsFn && !hasDynRef) {
|
|
908
958
|
// abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
|
|
909
959
|
// This is the perf-critical path for edge gateways. The richErrors wrap
|
|
@@ -920,10 +970,22 @@ class Validator {
|
|
|
920
970
|
? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
|
|
921
971
|
: (data) => _fn(data) ? _R : _efn(data);
|
|
922
972
|
} else if (hasDynRef) {
|
|
923
|
-
// $dynamicRef without codegen:
|
|
973
|
+
// $dynamicRef without codegen: the interpreted engine. It scores the
|
|
974
|
+
// same on the suite's $dynamicRef cases as the native walker since the
|
|
975
|
+
// dynamic-scope fix, needs no addon, and gets the verdict-only mode.
|
|
976
|
+
if (!_interp) {
|
|
977
|
+
const { createInterpreter } = require('./lib/interpreter');
|
|
978
|
+
_interp = createInterpreter(schemaObj, {
|
|
979
|
+
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
980
|
+
formats: this._userFormats,
|
|
981
|
+
v1: isV1Dialect(schemaObj),
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
const interp = _interp;
|
|
985
|
+
this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
|
|
924
986
|
this.validate = preprocess
|
|
925
|
-
? (data) => { preprocess(data); return
|
|
926
|
-
:
|
|
987
|
+
? (data) => { preprocess(data); return interp.validate(data); }
|
|
988
|
+
: (data) => interp.validate(data);
|
|
927
989
|
} else if (jsFn && jsFn._hybridFactory) {
|
|
928
990
|
// Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
|
|
929
991
|
// No arrow function wrapper, no ternary, one function call
|
|
@@ -1098,15 +1160,11 @@ class Validator {
|
|
|
1098
1160
|
// propertyDependencies exists only in the interpreted engine, so a schema
|
|
1099
1161
|
// using it goes there even when it also uses $dynamicRef.
|
|
1100
1162
|
const _hasPropDeps = this._schemaStr.includes('"propertyDependencies"')
|
|
1163
|
+
// $dynamicRef used to delegate to the native validateJSON path here;
|
|
1164
|
+
// the interpreted engine now scores the same on those cases, carries
|
|
1165
|
+
// the verdict-only mode, and works without the addon.
|
|
1101
1166
|
let _validate;
|
|
1102
|
-
|
|
1103
|
-
// validateJSON is the C++ path with full anchor-map support; the NAPI
|
|
1104
|
-
// direct V8 `validate` path has no anchor maps.
|
|
1105
|
-
this._engine = 'native';
|
|
1106
|
-
_validate = (data) => this._compiled.validateJSON(JSON.stringify(data));
|
|
1107
|
-
this.validateJSON = (jsonStr) => this._compiled.validateJSON(jsonStr);
|
|
1108
|
-
this.isValidJSON = (jsonStr) => this._compiled.isValidJSON(jsonStr);
|
|
1109
|
-
} else {
|
|
1167
|
+
{
|
|
1110
1168
|
const { createInterpreter } = require('./lib/interpreter');
|
|
1111
1169
|
const interp = createInterpreter(schemaObj, {
|
|
1112
1170
|
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
@@ -1115,6 +1173,7 @@ class Validator {
|
|
|
1115
1173
|
});
|
|
1116
1174
|
this._engine = 'interpreter';
|
|
1117
1175
|
_validate = (data) => interp.validate(data);
|
|
1176
|
+
this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
|
|
1118
1177
|
this.validateJSON = (jsonStr) => {
|
|
1119
1178
|
try {
|
|
1120
1179
|
return _validate(JSON.parse(jsonStr));
|
|
@@ -1130,7 +1189,9 @@ class Validator {
|
|
|
1130
1189
|
return _validate(data);
|
|
1131
1190
|
}
|
|
1132
1191
|
: _validate;
|
|
1133
|
-
this.isValidObject =
|
|
1192
|
+
this.isValidObject = this._fastVerdict
|
|
1193
|
+
? this._fastVerdict
|
|
1194
|
+
: (data) => _validate(data).valid;
|
|
1134
1195
|
this.validateAndParse = (jsonStr) => this._compiled.validateAndParse(jsonStr);
|
|
1135
1196
|
{
|
|
1136
1197
|
const slot = this._fastSlot;
|
|
@@ -1173,11 +1234,14 @@ class Validator {
|
|
|
1173
1234
|
v1: isV1Dialect(schemaObj),
|
|
1174
1235
|
});
|
|
1175
1236
|
this._engine = 'interpreter';
|
|
1237
|
+
if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
|
|
1176
1238
|
const run = preprocess
|
|
1177
1239
|
? (data) => { preprocess(data); return interp.validate(data); }
|
|
1178
1240
|
: (data) => interp.validate(data);
|
|
1179
1241
|
this.validate = run;
|
|
1180
|
-
this.isValidObject =
|
|
1242
|
+
this.isValidObject = this._fastVerdict
|
|
1243
|
+
? this._fastVerdict
|
|
1244
|
+
: (data) => run(data).valid;
|
|
1181
1245
|
this.validateJSON = (jsonStr) => {
|
|
1182
1246
|
try {
|
|
1183
1247
|
return run(JSON.parse(jsonStr));
|
|
@@ -1188,42 +1252,44 @@ class Validator {
|
|
|
1188
1252
|
this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
|
|
1189
1253
|
}
|
|
1190
1254
|
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
1255
|
+
// Error presentation, one lazy layer: declaration-order sorting, rich
|
|
1256
|
+
// enrichment (received value, suggestions, source frames, docUrl), or the
|
|
1257
|
+
// raw v0.14 shape under `richErrors: false`. All of it is work a caller
|
|
1258
|
+
// that only reads `.valid` never sees, so it runs on first access to
|
|
1259
|
+
// `.errors` and is cached. One wrapper, one allocation per rejection.
|
|
1194
1260
|
if (this.validate) {
|
|
1195
1261
|
const inner = this.validate;
|
|
1262
|
+
const enrich = this._richErrors ? require('./lib/enrich-error').enrich : null;
|
|
1196
1263
|
const root = this._schemaObj;
|
|
1264
|
+
const self = this;
|
|
1197
1265
|
this.validate = (data) => {
|
|
1198
1266
|
const result = inner(data);
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
if (positions) this._posCache.reset();
|
|
1226
|
-
return { valid: false, errors: enriched };
|
|
1267
|
+
// abortEarly returns the shared ATA9000 stub; preserve it as-is so the
|
|
1268
|
+
// perf fast path stays allocation-free and the documented code stays stable.
|
|
1269
|
+
if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
|
|
1270
|
+
// Positions come from the raw input when validateJSON set one;
|
|
1271
|
+
// resolved eagerly since the cache is reset per call.
|
|
1272
|
+
const positions = (enrich && self._lastRawInput != null) ? self._posCache.get(self._lastRawInput) : null;
|
|
1273
|
+
if (positions) self._posCache.reset();
|
|
1274
|
+
let cached = null;
|
|
1275
|
+
return {
|
|
1276
|
+
valid: false,
|
|
1277
|
+
get errors() {
|
|
1278
|
+
if (cached === null) {
|
|
1279
|
+
let raw = result.errors || [];
|
|
1280
|
+
if (raw.length > 1) raw = sortErrorsBySchemaOrder(root, raw);
|
|
1281
|
+
cached = (enrich && raw.length)
|
|
1282
|
+
? raw.map((e) => enrich(e, {
|
|
1283
|
+
data,
|
|
1284
|
+
positions,
|
|
1285
|
+
schemaPositions: self._schemaPositions,
|
|
1286
|
+
schemaFile: self._source ? self._source.path : undefined,
|
|
1287
|
+
}))
|
|
1288
|
+
: raw;
|
|
1289
|
+
}
|
|
1290
|
+
return cached;
|
|
1291
|
+
},
|
|
1292
|
+
};
|
|
1227
1293
|
}
|
|
1228
1294
|
return result;
|
|
1229
1295
|
};
|
|
@@ -1231,7 +1297,7 @@ class Validator {
|
|
|
1231
1297
|
// validateJSON also enriches: set _lastRawInput so the position cache
|
|
1232
1298
|
// can lazily build a map for dataFrame attachment. Only validateJSON
|
|
1233
1299
|
// wires this — validate(data) takes a pre-parsed object, by design.
|
|
1234
|
-
if (this.validateJSON) {
|
|
1300
|
+
if (this._richErrors && this.validateJSON) {
|
|
1235
1301
|
const innerJson = this.validateJSON;
|
|
1236
1302
|
this.validateJSON = (jsonStr) => {
|
|
1237
1303
|
this._lastRawInput = jsonStr;
|
|
@@ -1335,6 +1401,34 @@ class Validator {
|
|
|
1335
1401
|
}
|
|
1336
1402
|
}
|
|
1337
1403
|
|
|
1404
|
+
// Errors are paid for when read, not when produced. The full pipeline
|
|
1405
|
+
// above (error codegen, enrichment, custom messages, verbose) stays
|
|
1406
|
+
// intact, but validate() now answers the verdict from the boolean
|
|
1407
|
+
// engine and materializes `errors` through a getter on first access.
|
|
1408
|
+
// A caller that only reads `.valid`, which is every gateway check and
|
|
1409
|
+
// every benchmark, skips error construction entirely; a caller that
|
|
1410
|
+
// reads `.errors` pays once and the result is cached. Skipped when the
|
|
1411
|
+
// schema coerces or defaults (preprocess mutates before the verdict),
|
|
1412
|
+
// under abortEarly (already a frozen stub), and for $dynamicRef (the
|
|
1413
|
+
// boolean engine is not the authority there).
|
|
1414
|
+
if (this._fastVerdict && !preprocess && !options.abortEarly && this.validate) {
|
|
1415
|
+
const _full = this.validate;
|
|
1416
|
+
const _fast = this._fastVerdict;
|
|
1417
|
+
const EMPTY_ERRORS = Object.freeze([]);
|
|
1418
|
+
const _buildErrors = (data) => {
|
|
1419
|
+
const r = _full(data);
|
|
1420
|
+
return (r && r.valid === false && r.errors && r.errors.length)
|
|
1421
|
+
? r.errors
|
|
1422
|
+
// The data changed between the verdict and this read; keep the
|
|
1423
|
+
// verdict and say so rather than inventing a specific error.
|
|
1424
|
+
: [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
|
|
1425
|
+
};
|
|
1426
|
+
this.validate = (data) => {
|
|
1427
|
+
if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
|
|
1428
|
+
return new LazyRejection(_buildErrors, data);
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1338
1432
|
// The buffer APIs answer from the native walker, which disagrees with
|
|
1339
1433
|
// validate() on shapes listed in lib/buffer-gate.js. For those schemas
|
|
1340
1434
|
// every buffer entry point goes through validate() instead.
|
package/lib/aot.js
CHANGED
|
@@ -21,7 +21,7 @@ const SAFE_REGEX_SOURCE = require('./safe-regex-source');
|
|
|
21
21
|
const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
22
22
|
const len = s.length;
|
|
23
23
|
for (let i = 0; i < len; i++) {
|
|
24
|
-
if (s.charCodeAt(i)
|
|
24
|
+
if (((s.charCodeAt(i) - 0xD800) >>> 0) < 0x400) {
|
|
25
25
|
let n = 0; for (const _ of s) n++; return n;
|
|
26
26
|
}
|
|
27
27
|
}
|