ata-validator 1.47.0 → 1.48.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 +19 -0
- package/lib/enrich-error.js +46 -1
- package/lib/js-compiler-errors.js +97 -4
- package/lib/js-compiler.js +69 -5
- package/lib/rejections.js +27 -11
- package/lib/scan-compiler.js +14 -6
- package/lib/validator-core.js +5 -1
- package/lib/version.js +1 -1
- package/lite.d.ts +12 -0
- package/lite.js +20 -0
- package/lite.mjs +3 -0
- package/package.json +17 -8
package/README.md
CHANGED
|
@@ -43,6 +43,16 @@ npm install ata-validator --omit=optional
|
|
|
43
43
|
|
|
44
44
|
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
|
|
45
45
|
|
|
46
|
+
Where the size of what ships matters more than nanoseconds per call, `ata-validator/lite` is
|
|
47
|
+
the same `Validator` on the interpreted engine alone, without the code generator: the same
|
|
48
|
+
verdicts and errors, 58 KB gzipped in a bundled worker against 136 KB for the full entry.
|
|
49
|
+
It leaves out what needs the generator, `parse()` and the ahead-of-time bundle methods.
|
|
50
|
+
|
|
51
|
+
On Deno the same package is `npm:ata-validator` or `jsr:@ata/validator`; the JSR package
|
|
52
|
+
re-exports the npm one at the same version, and CI runs the official suite under Deno. The
|
|
53
|
+
details, and what Deno Deploy's refusal of dynamic code means for it, are in
|
|
54
|
+
[docs/edge-runtimes.md](docs/edge-runtimes.md).
|
|
55
|
+
|
|
46
56
|
Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
|
|
47
57
|
|
|
48
58
|
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1301 of 1301 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
|
|
@@ -280,6 +290,15 @@ const result = v.validate(data)
|
|
|
280
290
|
|
|
281
291
|
The runtime API is unchanged from previous releases. Code written against the default validator's class keeps working through `ata-validator/compat`, which covers `compile`, `addSchema`, `addFormat`, `addKeyword`, `errorsText` and the rest of that surface, and reports errors in the same shape and order. `npx ata migrate` reports what the switch would change in a project, and what it cannot translate, before anything is written. See [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
|
|
282
292
|
|
|
293
|
+
Coming from the default validator, one command reports what the switch touches and a second applies the mechanical part:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
npx ata migrate # per file: imports to rewrite, addFormats() calls that go, sites to look at by hand
|
|
297
|
+
npx ata migrate --write # rewrites the imports to ata-validator/compat and removes the calls, leaves the rest
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Coercion, defaults, `allErrors` and the error objects behave as before; the details and the limits are in [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
|
|
301
|
+
|
|
283
302
|
## Usage
|
|
284
303
|
|
|
285
304
|
### Node.js
|
package/lib/enrich-error.js
CHANGED
|
@@ -254,9 +254,17 @@ function enrich (rawErr, opts) {
|
|
|
254
254
|
// that answered wrote, the generator's internal ordering key included.
|
|
255
255
|
if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => enrich(b, data !== undefined ? { data } : null));
|
|
256
256
|
|
|
257
|
+
// The frames come from the position map, which is a walk of the document.
|
|
258
|
+
// Given lazily (validateJSON's rejections), the walk waits for the first
|
|
259
|
+
// read of any error's `dataFrame` or `anchor`: a caller that reads the
|
|
260
|
+
// messages and paths never pays for it, and one that reads every frame
|
|
261
|
+
// pays once, as before. Given eagerly, the fields are set as before.
|
|
262
|
+
const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
|
|
257
263
|
if (positions && positions[path]) {
|
|
258
264
|
const p = positions[path];
|
|
259
265
|
out.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
|
|
266
|
+
} else if (!positions && opts && opts.lazyPositions) {
|
|
267
|
+
defineLazyFrames(out, path, named, opts.lazyPositions);
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
// Attach schema source frame when the validator was constructed with a
|
|
@@ -286,7 +294,6 @@ function enrich (rawErr, opts) {
|
|
|
286
294
|
// adds the key span so a caret can sit on the property that is wrong rather
|
|
287
295
|
// than on the object containing it.
|
|
288
296
|
if (opts && opts.positions) {
|
|
289
|
-
const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
|
|
290
297
|
const own = opts.positions[path];
|
|
291
298
|
const child = named ? opts.positions[(path === '' ? '' : path) + '/' + named] : null;
|
|
292
299
|
const src = child || own;
|
|
@@ -303,4 +310,42 @@ function enrich (rawErr, opts) {
|
|
|
303
310
|
return out;
|
|
304
311
|
}
|
|
305
312
|
|
|
313
|
+
// `dataFrame` and `anchor` as accessors that resolve the position map on the
|
|
314
|
+
// first read of either, on any error of the list (the map is shared through
|
|
315
|
+
// `lazy`), and then become plain data properties, or go away when the map has
|
|
316
|
+
// no entry for the error, which is the shape the eager path gives. Both are
|
|
317
|
+
// enumerable, so JSON.stringify and a deep comparison read them as values;
|
|
318
|
+
// a write replaces the accessor the way an assignment to a plain field would.
|
|
319
|
+
function defineLazyFrames (out, path, named, lazy) {
|
|
320
|
+
let done = false;
|
|
321
|
+
let frames = null;
|
|
322
|
+
const resolve = () => {
|
|
323
|
+
if (done) return frames;
|
|
324
|
+
done = true;
|
|
325
|
+
const positions = lazy();
|
|
326
|
+
const own = positions ? positions[path] : null;
|
|
327
|
+
const dataFrame = own ? { byteOffset: own.byteOffset, length: own.length, line: own.line, col: own.col, text: own.text } : undefined;
|
|
328
|
+
const child = positions && named ? positions[(path === '' ? '' : path) + '/' + named] : null;
|
|
329
|
+
const src = child || own;
|
|
330
|
+
let anchor;
|
|
331
|
+
if (src) {
|
|
332
|
+
anchor = { line: src.line, col: src.col, length: src.length };
|
|
333
|
+
if (src.keyOffset !== undefined) {
|
|
334
|
+
anchor.keyLine = src.keyLine;
|
|
335
|
+
anchor.keyCol = src.keyCol;
|
|
336
|
+
anchor.keyLength = src.keyLength;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
frames = { dataFrame, anchor };
|
|
340
|
+
if (dataFrame !== undefined) Object.defineProperty(out, 'dataFrame', { value: dataFrame, enumerable: true, configurable: true, writable: true });
|
|
341
|
+
else delete out.dataFrame;
|
|
342
|
+
if (anchor !== undefined) Object.defineProperty(out, 'anchor', { value: anchor, enumerable: true, configurable: true, writable: true });
|
|
343
|
+
else delete out.anchor;
|
|
344
|
+
return frames;
|
|
345
|
+
};
|
|
346
|
+
const settable = (name) => (v) => Object.defineProperty(out, name, { value: v, enumerable: true, configurable: true, writable: true });
|
|
347
|
+
Object.defineProperty(out, 'dataFrame', { get () { return resolve().dataFrame; }, set: settable('dataFrame'), enumerable: true, configurable: true });
|
|
348
|
+
Object.defineProperty(out, 'anchor', { get () { return resolve().anchor; }, set: settable('anchor'), enumerable: true, configurable: true });
|
|
349
|
+
}
|
|
350
|
+
|
|
306
351
|
module.exports = { enrich, reprValue, expectedFor, detailFor, rankFor, MISSING, DOC_BASE };
|
|
@@ -1351,9 +1351,15 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1351
1351
|
if (schema.properties) {
|
|
1352
1352
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1353
1353
|
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
1354
|
+
const sp = schemaPrefix+'/properties/'+ptrSeg(key)
|
|
1355
|
+
const from = lines.length
|
|
1354
1356
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
1355
|
-
|
|
1357
|
+
// A large subschema as a function of its own (emitPropFnE); false means inline.
|
|
1358
|
+
if (!propFnCandidateE(prop, ctx, sp) || !emitPropFnE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)) {
|
|
1359
|
+
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)
|
|
1360
|
+
}
|
|
1356
1361
|
lines.push(`}`)
|
|
1362
|
+
countInlineE(lines, from, ctx)
|
|
1357
1363
|
}
|
|
1358
1364
|
}
|
|
1359
1365
|
|
|
@@ -2039,6 +2045,86 @@ function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2039
2045
|
return true
|
|
2040
2046
|
}
|
|
2041
2047
|
|
|
2048
|
+
// The same split for the error collector (genCodeE): a large property
|
|
2049
|
+
// subschema becomes `_pe<n>(d,_p,_all,_e)` in the helper code, pushing into
|
|
2050
|
+
// the caller's list as an inlined body would, generated against the real
|
|
2051
|
+
// schema path since it has one call site. Not inside a shared definition's
|
|
2052
|
+
// body, whose placeholder path is substituted after the fact.
|
|
2053
|
+
function propFnCandidateE (prop, ctx, schemaPrefix) {
|
|
2054
|
+
if (!ctx || ctx.nestedBoolean || ctx.condDepth) return false
|
|
2055
|
+
if (typeof schemaPrefix !== 'string' || schemaPrefix.includes(SP_PLACEHOLDER)) return false
|
|
2056
|
+
if (!prop || typeof prop !== 'object' || Array.isArray(prop)) return false
|
|
2057
|
+
let size = 0
|
|
2058
|
+
try { size = JSON.stringify(prop).length } catch { return false }
|
|
2059
|
+
return size >= SPLIT_C_SCHEMA || ((ctx.fnBytesE || 0) > SPLIT_C_BUDGET && size >= SPLIT_C_SMALL)
|
|
2060
|
+
}
|
|
2061
|
+
function emitPropFnE (prop, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
2062
|
+
const outerBytes = ctx.fnBytesE
|
|
2063
|
+
ctx.fnBytesE = 0
|
|
2064
|
+
const bodyLines = []
|
|
2065
|
+
try { genCodeE(prop, 'd', '_p', bodyLines, ctx, schemaPrefix) } finally { ctx.fnBytesE = outerBytes }
|
|
2066
|
+
if (bodyLines.length === 0) return false
|
|
2067
|
+
const body = bodyLines.join('\n ')
|
|
2068
|
+
if (body.includes(SP_PLACEHOLDER)) return false
|
|
2069
|
+
const fnName = '_pe' + (ctx.propFnsE = (ctx.propFnsE || 0) + 1)
|
|
2070
|
+
ctx.helperCode.push(`function ${fnName}(d,_p,_all,_e){${body}}`)
|
|
2071
|
+
lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e);if(!_all&&_e.length)return{valid:false,errors:_e}`)
|
|
2072
|
+
return true
|
|
2073
|
+
}
|
|
2074
|
+
function countInlineE (lines, from, ctx) {
|
|
2075
|
+
let n = 0
|
|
2076
|
+
for (let i = from; i < lines.length; i++) n += lines[i].length
|
|
2077
|
+
ctx.fnBytesE = (ctx.fnBytesE || 0) + n
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// A large property subschema as a function of its own, for the reason the
|
|
2081
|
+
// verdict generator has (emitPropFn in js-compiler.js): one function per
|
|
2082
|
+
// schema outgrew what V8 optimizes, and a 167 KB configuration schema's
|
|
2083
|
+
// one-pass function was 3.8 MB of source that ran in the interpreter. The
|
|
2084
|
+
// function is used at one site, so it is generated against the real schema
|
|
2085
|
+
// path and its errors keep their precise ordinal keys; it is not written
|
|
2086
|
+
// inside a shared definition's body, whose placeholder path is substituted
|
|
2087
|
+
// after the fact and would not reach a helper of its own.
|
|
2088
|
+
const SPLIT_C_SCHEMA = 1500
|
|
2089
|
+
const SPLIT_C_SMALL = 160
|
|
2090
|
+
const SPLIT_C_BUDGET = 16 * 1024
|
|
2091
|
+
function propFnCandidateC (prop, ctx, schemaPrefix) {
|
|
2092
|
+
if (!ctx || ctx.inDefC || ctx.nestedBoolean || ctx.condDepth) return false
|
|
2093
|
+
if (typeof schemaPrefix !== 'string' || schemaPrefix.includes(SP_PLACEHOLDER)) return false
|
|
2094
|
+
if (!prop || typeof prop !== 'object' || Array.isArray(prop)) return false
|
|
2095
|
+
let size = 0
|
|
2096
|
+
try { size = JSON.stringify(prop).length } catch { return false }
|
|
2097
|
+
return size >= SPLIT_C_SCHEMA || ((ctx.fnBytesC || 0) > SPLIT_C_BUDGET && size >= SPLIT_C_SMALL)
|
|
2098
|
+
}
|
|
2099
|
+
function emitPropFnC (prop, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
2100
|
+
const outerHoisted = ctx.hoisted
|
|
2101
|
+
ctx.hoisted = []
|
|
2102
|
+
const outerBytes = ctx.fnBytesC
|
|
2103
|
+
ctx.fnBytesC = 0
|
|
2104
|
+
const bodyLines = []
|
|
2105
|
+
let hoisted
|
|
2106
|
+
try {
|
|
2107
|
+
genGuardedC(prop, '_dv', '_p', bodyLines, ctx, schemaPrefix)
|
|
2108
|
+
} finally {
|
|
2109
|
+
hoisted = ctx.hoisted
|
|
2110
|
+
ctx.hoisted = outerHoisted
|
|
2111
|
+
ctx.fnBytesC = outerBytes
|
|
2112
|
+
}
|
|
2113
|
+
if (bodyLines.length === 0) return false
|
|
2114
|
+
const body = bodyLines.join('\n ')
|
|
2115
|
+
if (body.includes(SP_PLACEHOLDER) || /\b_so\b/.test(body)) return false
|
|
2116
|
+
const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
|
|
2117
|
+
const fnName = '_pc' + (ctx.propFnsC = (ctx.propFnsC || 0) + 1)
|
|
2118
|
+
ctx.helperCode.push(`function ${fnName}(_dv,_p,d){const _all=true;${decl}let _e;\n ${body}\n return _e}`)
|
|
2119
|
+
lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},d);if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
|
|
2120
|
+
return true
|
|
2121
|
+
}
|
|
2122
|
+
function countInlineC (lines, from, ctx) {
|
|
2123
|
+
let n = 0
|
|
2124
|
+
for (let i = from; i < lines.length; i++) n += lines[i].length
|
|
2125
|
+
ctx.fnBytesC = (ctx.fnBytesC || 0) + n
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2042
2128
|
// One schema as a function of its own in the combined function (see
|
|
2043
2129
|
// emitSharedDefC). The name is registered before the body is written, so a
|
|
2044
2130
|
// reference back to it inside becomes a call. One that can re-enter itself,
|
|
@@ -2507,21 +2593,28 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2507
2593
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
2508
2594
|
const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
2509
2595
|
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
2596
|
+
const sp = schemaPrefix+'/properties/'+ptrSeg(key)
|
|
2597
|
+
const from = lines.length
|
|
2598
|
+
const split = propFnCandidateC(prop, ctx, sp)
|
|
2599
|
+
// A large subschema as a function of its own (emitPropFnC), called
|
|
2600
|
+
// where the inline code would be; false means it is written inline.
|
|
2601
|
+
const sub = (pvExpr) => { if (!split || !emitPropFnC(prop, pvExpr, childPath, lines, ctx, sp)) genGuardedC(prop, pvExpr, childPath, lines, ctx, sp) }
|
|
2510
2602
|
if (requiredSet.has(key) && isObj) {
|
|
2511
2603
|
// A hoisted local was already cleared if the value is only inherited.
|
|
2512
2604
|
lines.push(`if(${pv}!==undefined${hoisted[key] ? '' : '&&' + ownGuard(ctx, v, key)}){`)
|
|
2513
|
-
|
|
2605
|
+
sub(pv)
|
|
2514
2606
|
lines.push(`}`)
|
|
2515
2607
|
} else if (isObj) {
|
|
2516
2608
|
const oi = ctx.varCounter++
|
|
2517
2609
|
lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined&&${ownGuard(ctx, v, key)}){`)
|
|
2518
|
-
|
|
2610
|
+
sub(`_o${oi}`)
|
|
2519
2611
|
lines.push(`}}`)
|
|
2520
2612
|
} else {
|
|
2521
2613
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
2522
|
-
|
|
2614
|
+
sub(`${v}[${JSON.stringify(key)}]`)
|
|
2523
2615
|
lines.push(`}`)
|
|
2524
2616
|
}
|
|
2617
|
+
countInlineC(lines, from, ctx)
|
|
2525
2618
|
}
|
|
2526
2619
|
}
|
|
2527
2620
|
|
package/lib/js-compiler.js
CHANGED
|
@@ -1161,6 +1161,60 @@ function _setCodeBudget (n) { codeBudget = n === undefined ? CODE_BUDGET : n }
|
|
|
1161
1161
|
// document that reached it, and the second cut the cycle, so the reference
|
|
1162
1162
|
// checked nothing. While `nestedBoolean` is set, genCode declines at those
|
|
1163
1163
|
// points instead, and the caller's generator declines the schema.
|
|
1164
|
+
// A property whose subschema is large becomes a function of its own in the
|
|
1165
|
+
// preamble, called where the property is checked, instead of its code inline.
|
|
1166
|
+
// V8 does not optimize a function past its bytecode budget (about 60 KB), and
|
|
1167
|
+
// a configuration schema of a few hundred properties compiled to one function
|
|
1168
|
+
// of a megabyte or more that ran in the interpreter for its whole life: on a
|
|
1169
|
+
// 167 KB schema the verdict took 407 us where the same checks in functions of
|
|
1170
|
+
// their own take 40. The threshold is on the schema's own size, which is
|
|
1171
|
+
// known before any code is written, so nothing is generated twice. A
|
|
1172
|
+
// property moves out on its own only when its schema is plainly large; the
|
|
1173
|
+
// usual case is the budget: once a function holds SPLIT_FN_BUDGET bytes of
|
|
1174
|
+
// inline code, every property past SPLIT_PROP_SMALL moves out, so a schema
|
|
1175
|
+
// of many middling properties splits after its first functions' worth and a
|
|
1176
|
+
// small or medium one, whose whole program fits the budget, never does and
|
|
1177
|
+
// pays no call.
|
|
1178
|
+
const SPLIT_PROP_SCHEMA = 2000
|
|
1179
|
+
const SPLIT_PROP_SMALL = 220
|
|
1180
|
+
const SPLIT_FN_BUDGET = 24 * 1024
|
|
1181
|
+
function propSchemaSize (prop) {
|
|
1182
|
+
try { return JSON.stringify(prop).length } catch { return 0 }
|
|
1183
|
+
}
|
|
1184
|
+
function propFnCandidate (prop, ctx) {
|
|
1185
|
+
if (!ctx.preamble || ctx.nestedBoolean || ctx.condDepth) return false
|
|
1186
|
+
if (!prop || typeof prop !== 'object' || Array.isArray(prop)) return false
|
|
1187
|
+
const size = propSchemaSize(prop)
|
|
1188
|
+
return size >= SPLIT_PROP_SCHEMA || ((ctx.fnBytes || 0) > SPLIT_FN_BUDGET && size >= SPLIT_PROP_SMALL)
|
|
1189
|
+
}
|
|
1190
|
+
// Generated over its own `d`, the way a shared definition is (see the $ref
|
|
1191
|
+
// case in genCodeNode): the deferred checks belong to this body, and the
|
|
1192
|
+
// plain-object reuse state must not leak in or out. Null means "write it
|
|
1193
|
+
// inline after all": an empty body (the inline path then asserts it the
|
|
1194
|
+
// usual way) or one that calls the recursive entry, which is out of scope
|
|
1195
|
+
// in the preamble.
|
|
1196
|
+
function emitPropFn (prop, ctx) {
|
|
1197
|
+
const bodyLines = []
|
|
1198
|
+
const outerDeferred = ctx.deferredChecks
|
|
1199
|
+
ctx.deferredChecks = null
|
|
1200
|
+
const outerReuse = ctx.plainReuse
|
|
1201
|
+
ctx.plainReuse = undefined
|
|
1202
|
+
const outerBytes = ctx.fnBytes
|
|
1203
|
+
ctx.fnBytes = 0
|
|
1204
|
+
try { genCode(prop, 'd', bodyLines, ctx) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes }
|
|
1205
|
+
if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
|
|
1206
|
+
ctx.deferredChecks = outerDeferred
|
|
1207
|
+
if (bodyLines.length === 0 || bodyLines.some((l) => /\b_validate\b/.test(l))) return null
|
|
1208
|
+
const fnName = '_pf' + (ctx.propFns = (ctx.propFns || 0) + 1)
|
|
1209
|
+
hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
|
|
1210
|
+
return fnName
|
|
1211
|
+
}
|
|
1212
|
+
function countInline (lines, from, ctx) {
|
|
1213
|
+
let n = 0
|
|
1214
|
+
for (let i = from; i < lines.length; i++) n += lines[i].length
|
|
1215
|
+
ctx.fnBytes = (ctx.fnBytes || 0) + n
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1164
1218
|
function nestedGenCode(schema, v, lines, ctx) {
|
|
1165
1219
|
ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
|
|
1166
1220
|
const before = lines.length
|
|
@@ -3498,11 +3552,17 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3498
3552
|
// properties — use hoisted vars for required props, hoist optional to locals too
|
|
3499
3553
|
if (schema.properties) {
|
|
3500
3554
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3555
|
+
const from = lines.length
|
|
3556
|
+
// A large subschema as a function of its own (emitPropFn); the call
|
|
3557
|
+
// takes the place of the inline code in each of the three shapes below.
|
|
3558
|
+
const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
|
|
3501
3559
|
if (requiredSet.has(key) && isObj) {
|
|
3502
3560
|
// Required + type:object — hoist to local to reduce repeated property lookups
|
|
3503
3561
|
const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
3504
|
-
const combined = tryGenCombined(prop, access, ctx)
|
|
3505
|
-
if (
|
|
3562
|
+
const combined = fn ? null : tryGenCombined(prop, access, ctx)
|
|
3563
|
+
if (fn) {
|
|
3564
|
+
lines.push(`if(!${fn}(${access}))return false`)
|
|
3565
|
+
} else if (combined) {
|
|
3506
3566
|
lines.push(combined)
|
|
3507
3567
|
} else if (needsLocal(prop) || isPlainContainer(prop)) {
|
|
3508
3568
|
const oi = ctx.varCounter++
|
|
@@ -3518,8 +3578,10 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3518
3578
|
const oi = ctx.varCounter++
|
|
3519
3579
|
const local = `_o${oi}`
|
|
3520
3580
|
lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined&&${ownGuard(ctx, v, key)}){`)
|
|
3521
|
-
const combined = tryGenCombined(prop, local, ctx)
|
|
3522
|
-
if (
|
|
3581
|
+
const combined = fn ? null : tryGenCombined(prop, local, ctx)
|
|
3582
|
+
if (fn) {
|
|
3583
|
+
lines.push(`if(!${fn}(${local}))return false`)
|
|
3584
|
+
} else if (combined) {
|
|
3523
3585
|
lines.push(combined)
|
|
3524
3586
|
} else {
|
|
3525
3587
|
genCode(prop, local, lines, ctx)
|
|
@@ -3527,9 +3589,11 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3527
3589
|
lines.push(`}}`)
|
|
3528
3590
|
} else {
|
|
3529
3591
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
|
|
3530
|
-
|
|
3592
|
+
if (fn) lines.push(`if(!${fn}(${v}[${JSON.stringify(key)}]))return false`)
|
|
3593
|
+
else genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
|
|
3531
3594
|
lines.push(`}`)
|
|
3532
3595
|
}
|
|
3596
|
+
countInline(lines, from, ctx)
|
|
3533
3597
|
}
|
|
3534
3598
|
}
|
|
3535
3599
|
|
package/lib/rejections.js
CHANGED
|
@@ -84,17 +84,16 @@ class RichRejection {
|
|
|
84
84
|
// legacy key set, or enriched. Shared by both rejection classes below.
|
|
85
85
|
function presentErrors(raw, data, rawInput, self, root, enrich) {
|
|
86
86
|
if (needsOrdering(raw)) raw = sortErrorsBySchemaOrder(root, raw);
|
|
87
|
-
// The position map, resolved
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (positions) self._posCache.reset();
|
|
92
|
-
}
|
|
87
|
+
// The position map, resolved on the first read of an error's frame (see
|
|
88
|
+
// defineLazyFrames in lib/enrich-error.js): a walk of the document that a
|
|
89
|
+
// caller reading messages and paths never needs.
|
|
90
|
+
const lazyPositions = enrich && raw.length && rawInput != null ? lazyPositionsFor(self, rawInput, raw) : null;
|
|
93
91
|
// One options object for the whole list, not one per error.
|
|
94
92
|
const opts = enrich && raw.length
|
|
95
93
|
? {
|
|
96
94
|
data,
|
|
97
|
-
positions,
|
|
95
|
+
positions: null,
|
|
96
|
+
lazyPositions,
|
|
98
97
|
schemaPositions: self._schemaPositions,
|
|
99
98
|
schemaFile: self._source ? self._source.path : undefined,
|
|
100
99
|
}
|
|
@@ -180,13 +179,16 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
180
179
|
// on raw errors; taking it as the signal left those errors without
|
|
181
180
|
// `received` or a suggestion on the text path while validate() had both.
|
|
182
181
|
if (!raw[0] || !('received' in raw[0])) {
|
|
183
|
-
const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
|
|
184
182
|
// Declaration order, as validate() applies it; the text path used to
|
|
185
183
|
// enrich in emission order.
|
|
186
184
|
const ordered = needsOrdering(raw) ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
|
|
185
|
+
// The position map waits for the first read of a frame (lib/enrich-error
|
|
186
|
+
// defineLazyFrames); until then reading the list costs the enrichment alone.
|
|
187
|
+
const lazyPositions = lazyPositionsFor(self, jsonStr, ordered);
|
|
187
188
|
const enrichOpts = {
|
|
188
189
|
data: parsedData,
|
|
189
|
-
positions,
|
|
190
|
+
positions: null,
|
|
191
|
+
lazyPositions,
|
|
190
192
|
schemaPositions: self._schemaPositions,
|
|
191
193
|
schemaFile: self._source ? self._source.path : undefined,
|
|
192
194
|
};
|
|
@@ -195,11 +197,11 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
195
197
|
attachDiagnosticSource(enriched, {
|
|
196
198
|
data: parsedData,
|
|
197
199
|
text: jsonStr,
|
|
198
|
-
|
|
200
|
+
// Read by the diagnostic renderer only, which resolves the same map.
|
|
201
|
+
get positions () { return lazyPositions(); },
|
|
199
202
|
schema: self._schemaObj,
|
|
200
203
|
mutatesInput: self._mutatesInput === true,
|
|
201
204
|
});
|
|
202
|
-
if (positions) self._posCache.reset();
|
|
203
205
|
this._cached = enriched;
|
|
204
206
|
return enriched;
|
|
205
207
|
}
|
|
@@ -232,6 +234,20 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
232
234
|
},
|
|
233
235
|
});
|
|
234
236
|
|
|
237
|
+
// The targeted position map for a list, built on the first call and shared
|
|
238
|
+
// by every error's frame accessor (lib/enrich-error defineLazyFrames).
|
|
239
|
+
function lazyPositionsFor (self, text, errors) {
|
|
240
|
+
let done = false;
|
|
241
|
+
let positions = null;
|
|
242
|
+
return () => {
|
|
243
|
+
if (done) return positions;
|
|
244
|
+
done = true;
|
|
245
|
+
positions = self._pos().targeted(text, wantedPointersFor(errors));
|
|
246
|
+
if (positions) self._posCache.reset();
|
|
247
|
+
return positions;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
235
251
|
// The pointers a set of errors will ask the position map about. lib/enrich-error
|
|
236
252
|
// looks up the error's own path, and for an additional or unevaluated property
|
|
237
253
|
// the child pointer named in `params`, unescaped, which is the form it asks for.
|
package/lib/scan-compiler.js
CHANGED
|
@@ -892,7 +892,7 @@ function emitArray(schema, ctx, out, depth) {
|
|
|
892
892
|
// Every `return 0` inside the member becomes that record, which is also how
|
|
893
893
|
// the rule reaches nested objects: an inner object's own verdict is a value
|
|
894
894
|
// failure at the level above it.
|
|
895
|
-
function emitMember(sub, ctx, out, depth, objId) {
|
|
895
|
+
function emitMember(sub, ctx, out, depth, objId, kn) {
|
|
896
896
|
let lines = [];
|
|
897
897
|
emitValue(sub, ctx, lines, depth + 1);
|
|
898
898
|
// A large member value becomes a function of its own, so no one function
|
|
@@ -921,9 +921,17 @@ function emitMember(sub, ctx, out, depth, objId) {
|
|
|
921
921
|
out.push(label + ':{');
|
|
922
922
|
out.push(body.split('return 0;').join('{_f' + objId + '=2;break ' + label + ';}'));
|
|
923
923
|
out.push('}');
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
|
|
924
|
+
// A failed member is the verdict unless this object repeats the key later,
|
|
925
|
+
// since the parser keeps the last value. Whether it can is answered by two
|
|
926
|
+
// native searches over the rest of the text, for the key as written and
|
|
927
|
+
// for any backslash, which is the only way to spell that key differently:
|
|
928
|
+
// when neither is there the scan ends here, and a 69 KB body with a bad
|
|
929
|
+
// first item is refused in the time of an indexOf rather than a full pass.
|
|
930
|
+
// Otherwise resync from where the value started, since a scan that stopped
|
|
931
|
+
// partway through one cannot say where it ends and the skipper can, and
|
|
932
|
+
// go on, so a repeat still bails to the parser.
|
|
933
|
+
out.push('if(_f' + objId + '===2){if(s.indexOf(s.slice(_p' + kn + '-1,_e' + kn + '+1),i)<0&&s.indexOf("\\\\",i)<0)return 0;');
|
|
934
|
+
out.push('_f' + objId + '=1;i=_r.skipValue(s,_vs' + label + ');if(i<0)return 0;c=s.charCodeAt(i);}');
|
|
927
935
|
}
|
|
928
936
|
|
|
929
937
|
function emitObject(schema, ctx, out, depth) {
|
|
@@ -1111,7 +1119,7 @@ function emitObject(schema, ctx, out, depth) {
|
|
|
1111
1119
|
const cond = gi === bounds.length - 1 ? '' : 'if(_kid' + kn + '<' + g.end + ')';
|
|
1112
1120
|
out.push((gi === 0 ? '' : 'else ') + cond + '{');
|
|
1113
1121
|
if (g.sub === undefined) out.push(addH);
|
|
1114
|
-
else emitMember(g.sub, ctx, out, depth, n);
|
|
1122
|
+
else emitMember(g.sub, ctx, out, depth, n, kn);
|
|
1115
1123
|
out.push('}');
|
|
1116
1124
|
prev = g.end;
|
|
1117
1125
|
});
|
|
@@ -1160,7 +1168,7 @@ function emitObject(schema, ctx, out, depth) {
|
|
|
1160
1168
|
out.push('_s' + n + '_' + slot.w + '|=' + slot.b + ';');
|
|
1161
1169
|
const sub = props && Object.prototype.hasOwnProperty.call(props, name) ? props[name] : undefined;
|
|
1162
1170
|
if (sub === undefined) out.push(additional.join('\n'));
|
|
1163
|
-
else emitMember(sub, ctx, out, depth, n);
|
|
1171
|
+
else emitMember(sub, ctx, out, depth, n, kn);
|
|
1164
1172
|
out.push('}');
|
|
1165
1173
|
}
|
|
1166
1174
|
out.push('else{' + additional.join('\n') + '}');
|
package/lib/validator-core.js
CHANGED
|
@@ -1802,8 +1802,12 @@ Validator.loadBundle = function (mods, schemas, opts) {
|
|
|
1802
1802
|
// simdjson when the addon is there, JSON.parse otherwise; the addon loads on
|
|
1803
1803
|
// the first call rather than when the package is required.
|
|
1804
1804
|
function parseJSON(input) {
|
|
1805
|
+
// The addon has not carried parseJSON since the per-platform packages
|
|
1806
|
+
// (1.1.0); the call threw a TypeError whenever the engine was loaded and
|
|
1807
|
+
// returned JSON.parse's answer otherwise. A Buffer is decoded first.
|
|
1805
1808
|
const native = getNative();
|
|
1806
|
-
|
|
1809
|
+
if (native && typeof native.parseJSON === 'function') return native.parseJSON(input);
|
|
1810
|
+
return JSON.parse(typeof input === 'string' ? input : String(input));
|
|
1807
1811
|
}
|
|
1808
1812
|
|
|
1809
1813
|
// Ultra-fast compile: returns validate function directly, no Validator wrapper
|
package/lib/version.js
CHANGED
package/lite.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ata-validator/lite: the same Validator and one-shot functions as
|
|
2
|
+
// ata-validator, on the interpreted engine and without the code generator.
|
|
3
|
+
// Validator#parse and the static bundle methods throw a TypeError here.
|
|
4
|
+
export {
|
|
5
|
+
Validator,
|
|
6
|
+
validate,
|
|
7
|
+
validateAsync,
|
|
8
|
+
parseAsync,
|
|
9
|
+
defineSchema,
|
|
10
|
+
version,
|
|
11
|
+
} from './index.js';
|
|
12
|
+
export type * from './index.js';
|
package/lite.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ata-validator/lite: the Validator without the code generator. Every schema
|
|
4
|
+
// runs on the interpreted engine, which passes the same official test suite as
|
|
5
|
+
// the compiled path and never calls `new Function`, so it also runs under a
|
|
6
|
+
// strict Content-Security-Policy. The results are the ones `ata-validator`
|
|
7
|
+
// gives: the same verdicts, the same errors. What it leaves out is what needs
|
|
8
|
+
// the code generator (parse(), the ahead-of-time bundle methods) and the tools
|
|
9
|
+
// no Validator calls (TypeScript generation, the renderers). Made for pages
|
|
10
|
+
// where the size of what ships matters more than nanoseconds per call.
|
|
11
|
+
const core = require('./lib/validator-core.js');
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
Validator: core.Validator,
|
|
15
|
+
validate: core.validate,
|
|
16
|
+
validateAsync: core.validateAsync,
|
|
17
|
+
parseAsync: core.parseAsync,
|
|
18
|
+
defineSchema: core.defineSchema,
|
|
19
|
+
version: core.version,
|
|
20
|
+
};
|
package/lite.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.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
|
+
"type": "commonjs",
|
|
5
6
|
"main": "index.js",
|
|
6
7
|
"module": "index.mjs",
|
|
7
8
|
"types": "index.d.ts",
|
|
@@ -48,6 +49,11 @@
|
|
|
48
49
|
"import": "./compiled-verdict.mjs",
|
|
49
50
|
"require": "./compiled-verdict.js"
|
|
50
51
|
},
|
|
52
|
+
"./lite": {
|
|
53
|
+
"types": "./lite.d.ts",
|
|
54
|
+
"import": "./lite.mjs",
|
|
55
|
+
"require": "./lite.js"
|
|
56
|
+
},
|
|
51
57
|
"./package.json": "./package.json"
|
|
52
58
|
},
|
|
53
59
|
"sideEffects": false,
|
|
@@ -138,18 +144,21 @@
|
|
|
138
144
|
"compiled-verdict.js",
|
|
139
145
|
"compiled-verdict.mjs",
|
|
140
146
|
"compiled-verdict.d.ts",
|
|
147
|
+
"lite.js",
|
|
148
|
+
"lite.mjs",
|
|
149
|
+
"lite.d.ts",
|
|
141
150
|
"bin/",
|
|
142
151
|
"README.md",
|
|
143
152
|
"LICENSE"
|
|
144
153
|
],
|
|
145
154
|
"optionalDependencies": {
|
|
146
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
147
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
148
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
149
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
150
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
151
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
152
|
-
"@ata-validator/native-win32-x64": "1.
|
|
155
|
+
"@ata-validator/native-darwin-arm64": "1.48.0",
|
|
156
|
+
"@ata-validator/native-darwin-x64": "1.48.0",
|
|
157
|
+
"@ata-validator/native-linux-arm64-gnu": "1.48.0",
|
|
158
|
+
"@ata-validator/native-linux-arm64-musl": "1.48.0",
|
|
159
|
+
"@ata-validator/native-linux-x64-gnu": "1.48.0",
|
|
160
|
+
"@ata-validator/native-linux-x64-musl": "1.48.0",
|
|
161
|
+
"@ata-validator/native-win32-x64": "1.48.0"
|
|
153
162
|
},
|
|
154
163
|
"peerDependencies": {
|
|
155
164
|
"yaml": "^2.0.0"
|