ata-validator 1.57.0 → 1.58.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.
- package/README.md +3 -2
- package/lib/aot-impl.js +13 -2
- package/lib/aot-order.js +1 -1
- package/lib/buffer-gate.js +33 -0
- package/lib/clone-emit.js +11 -25
- package/lib/codegen-engine.js +147 -16
- package/lib/defaults.js +213 -56
- package/lib/error-codes.js +6 -0
- package/lib/formats-source.js +1 -1
- package/lib/interpreter-core.js +8 -2
- package/lib/interpreter.browser.js +34 -0
- package/lib/schema-strip.js +15 -8
- package/lib/validator-core.js +28 -17
- package/lib/version.js +1 -1
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -53,8 +53,9 @@ interpreted engine, which gives the same answers, and a validator called a thous
|
|
|
53
53
|
a second loads the generator in the background, through a dynamic import that Vite, webpack and
|
|
54
54
|
Rollup put in a chunk of their own, and is rebuilt on generated code when the page is idle. A
|
|
55
55
|
form, checked as the user types, never loads it. On a react-jsonschema-form page this takes the validator's
|
|
56
|
-
share of the first load from 134.7 KB to about
|
|
57
|
-
more when it does load
|
|
56
|
+
share of the first load from 134.7 KB to about 55 KB gzipped; the generator chunk is about 78 KB
|
|
57
|
+
more when it does load, and the plan compiler, which speeds up a schema the interpreted engine checks
|
|
58
|
+
often, comes in a 6 KB chunk once the page is idle. Where `unsafe-eval` is blocked, on a page with a strict
|
|
58
59
|
Content-Security-Policy or in a Manifest V3 extension, it is never fetched and every schema
|
|
59
60
|
stays on the interpreted engine. To have generated code from the first call, import
|
|
60
61
|
`ata-validator/codegen` once; it also makes `parse()` available in the browser, which otherwise
|
package/lib/aot-impl.js
CHANGED
|
@@ -355,9 +355,20 @@ const ABORT_SHAPE = Object.freeze({ valid: false, errors: Object.freeze([Object.
|
|
|
355
355
|
// default, or where the pass declines it (a schema object containing itself).
|
|
356
356
|
function defaultsDecl(validator) {
|
|
357
357
|
const { resolveSchemaForPreprocess } = require('./validator-core')._internals;
|
|
358
|
+
const schema = resolveSchemaForPreprocess(validator._schemaObj, validator._schemaMap);
|
|
358
359
|
let src = null;
|
|
359
|
-
try { src = require('./codegen-engine').preprocessSource(
|
|
360
|
-
|
|
360
|
+
try { src = require('./codegen-engine').preprocessSource(schema, { useDefaults: true }); } catch {}
|
|
361
|
+
if (src !== null) return `function applyDefaults(d) {\n${src}\n}\n`;
|
|
362
|
+
// The generated pass declines a recursive reference and keys matched by
|
|
363
|
+
// patterns; the runtime fills those with the closure pass, so the module
|
|
364
|
+
// carries it, as its own text (lib/defaults.js), with its schema less the
|
|
365
|
+
// descriptive keywords, `default` kept. Built on the first call.
|
|
366
|
+
const defaults = require('./defaults');
|
|
367
|
+
if (defaults.buildDefaultsApplier(schema) === null) return '';
|
|
368
|
+
const embedded = require('./schema-strip').stripAnnotationsKeepingDefaults(schema);
|
|
369
|
+
const text = JSON.stringify(JSON.stringify(embedded)).replace(/</g, '\\u003c');
|
|
370
|
+
return `const _ataDefaults = (function () {\nconst MAX_DEPTH = ${defaults.MAX_DEPTH};\n${require(AOT_ORDER_MODULE).take('defaults.js', defaults.EMBED)}\nreturn buildDefaultsApplier;\n})();\n` +
|
|
371
|
+
`let _ataFill;\nfunction applyDefaults(d) { (_ataFill || (_ataFill = _ataDefaults(JSON.parse(${text}))))(d); }\n`;
|
|
361
372
|
}
|
|
362
373
|
|
|
363
374
|
function toStandaloneModule(validator, opts) {
|
package/lib/aot-order.js
CHANGED
package/lib/buffer-gate.js
CHANGED
|
@@ -61,7 +61,39 @@ function re2Diverges(pattern) {
|
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// The walker gives up past a depth of its own (100 schema frames, about 50
|
|
65
|
+
// levels of data under a recursive schema) and rejects the document there. A
|
|
66
|
+
// schema that refers back into itself, or that is nested that deep, can reach
|
|
67
|
+
// it with a document validate() accepts, so it is answered through validate(),
|
|
68
|
+
// which has no such limit.
|
|
69
|
+
const NATIVE_DEPTH = 40;
|
|
70
|
+
|
|
71
|
+
// Whether a `$ref` into this document leads back to a schema it is inside.
|
|
72
|
+
function refersBack(root) {
|
|
73
|
+
const { localRef } = require('./defaults.js');
|
|
74
|
+
const done = new Set();
|
|
75
|
+
const onPath = new Set();
|
|
76
|
+
const visit = (node) => {
|
|
77
|
+
if (node === null || typeof node !== 'object') return false;
|
|
78
|
+
if (onPath.has(node)) return true;
|
|
79
|
+
if (done.has(node)) return false;
|
|
80
|
+
onPath.add(node);
|
|
81
|
+
let back = false;
|
|
82
|
+
for (const key of Object.keys(node)) {
|
|
83
|
+
const v = node[key];
|
|
84
|
+
if (key === '$ref' && typeof v === 'string' && v.startsWith('#')) back = visit(localRef(root, v));
|
|
85
|
+
else if (key !== 'enum' && key !== 'const' && key !== 'default' && key !== 'examples' && v !== null && typeof v === 'object') back = visit(v);
|
|
86
|
+
if (back) break;
|
|
87
|
+
}
|
|
88
|
+
onPath.delete(node);
|
|
89
|
+
done.add(node);
|
|
90
|
+
return back;
|
|
91
|
+
};
|
|
92
|
+
return visit(root);
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
function walk(schema, depth) {
|
|
96
|
+
if (depth > NATIVE_DEPTH) return true;
|
|
65
97
|
if (schema === true || schema === false) {
|
|
66
98
|
// A boolean root is answered wrong by the walker; nested booleans are
|
|
67
99
|
// handled where they appear (items and prefixItems below).
|
|
@@ -110,6 +142,7 @@ function walk(schema, depth) {
|
|
|
110
142
|
function bufferNeedsSlowPath(schema, schemaMap, keywords) {
|
|
111
143
|
if (keywords && schemaUsesKeywords(schema, keywords)) return true;
|
|
112
144
|
if (walk(schema, 0)) return true;
|
|
145
|
+
if (refersBack(schema)) return true;
|
|
113
146
|
if (schemaMap && schemaMap.size > 0) {
|
|
114
147
|
for (const s of schemaMap.values()) if (walk(s, 1)) return true;
|
|
115
148
|
}
|
package/lib/clone-emit.js
CHANGED
|
@@ -50,12 +50,13 @@ const _INLINE_ANNOTATIONS = new Set(['$ref', 'title', 'description', '$comment',
|
|
|
50
50
|
const _INLINE_DATA_KEYS = new Set(['default', 'const', 'enum', 'examples']);
|
|
51
51
|
function inlineRefsForClone(root) {
|
|
52
52
|
const state = { budget: 512 };
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// defaults
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
53
|
+
// The runtime's useDefaults fills a property's default from the schema
|
|
54
|
+
// written under the property, the $ref node with any default next to it,
|
|
55
|
+
// and then the defaults inside the referenced definition, as it fills them
|
|
56
|
+
// inline. The definition's own top-level default is not the property's, so
|
|
57
|
+
// the inlined copy drops that one and keeps everything below it. parse()
|
|
58
|
+
// must stay exactly as generous as validate(); a parse that fills more, or
|
|
59
|
+
// less, is a disagreement.
|
|
59
60
|
const walk = (node, active, inRef) => {
|
|
60
61
|
if (!node || typeof node !== 'object') return node;
|
|
61
62
|
if (Array.isArray(node)) return node.map((n) => walk(n, active, inRef));
|
|
@@ -73,14 +74,13 @@ function inlineRefsForClone(root) {
|
|
|
73
74
|
cur = target;
|
|
74
75
|
entered = true;
|
|
75
76
|
}
|
|
76
|
-
const strip = inRef || entered;
|
|
77
77
|
const out = {};
|
|
78
78
|
for (const k of Object.keys(cur)) {
|
|
79
|
-
if (
|
|
79
|
+
if (entered && k === 'default') continue;
|
|
80
80
|
const v = cur[k];
|
|
81
81
|
// A property named "__proto__" is an own key of the copy: assigned, it
|
|
82
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,
|
|
83
|
+
const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active, false);
|
|
84
84
|
if (k === '__proto__') Object.defineProperty(out, k, { value: w, writable: true, enumerable: true, configurable: true });
|
|
85
85
|
else out[k] = w;
|
|
86
86
|
}
|
|
@@ -197,7 +197,7 @@ function emitClone(node, access, depth) {
|
|
|
197
197
|
if (apSchema) {
|
|
198
198
|
const kVar = `_k${depth}`;
|
|
199
199
|
const vVar = `_v${depth}`;
|
|
200
|
-
const valueExpr = emitValueExpr(
|
|
200
|
+
const valueExpr = emitValueExpr(apSchema, vVar, depth);
|
|
201
201
|
if (valueExpr === null) return null;
|
|
202
202
|
let skip = '';
|
|
203
203
|
let declSet = '';
|
|
@@ -255,20 +255,6 @@ function emitClone(node, access, depth) {
|
|
|
255
255
|
// carry keys this code cannot see, and copying the reference would smuggle
|
|
256
256
|
// them into a value that claims to be sanitised, so the caller declines the
|
|
257
257
|
// whole clone instead.
|
|
258
|
-
// Drop every schema-position `default` in a subtree, leaving data positions
|
|
259
|
-
// (const, enum, examples) untouched. Used on a record's value schema, where
|
|
260
|
-
// the runtime's useDefaults fills nothing.
|
|
261
|
-
function stripDefaultsDeep(node) {
|
|
262
|
-
if (!node || typeof node !== 'object') return node;
|
|
263
|
-
if (Array.isArray(node)) return node.map(stripDefaultsDeep);
|
|
264
|
-
const out = {};
|
|
265
|
-
for (const k of Object.keys(node)) {
|
|
266
|
-
if (k === 'default') continue;
|
|
267
|
-
out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
|
|
268
|
-
}
|
|
269
|
-
return out;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
258
|
const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
|
|
273
259
|
// Whether every value this schema accepts is a primitive, so a copy has
|
|
274
260
|
// nothing inside it to strip and takes the value as it is. A primitive type
|
|
@@ -329,4 +315,4 @@ function cloneExprFor(schema) {
|
|
|
329
315
|
return emitClone(inlineRefsForClone(schema), 'data', 0);
|
|
330
316
|
}
|
|
331
317
|
|
|
332
|
-
module.exports = { cloneExprFor, emitClone, inlineRefsForClone,
|
|
318
|
+
module.exports = { cloneExprFor, emitClone, inlineRefsForClone, resolveLocalPointer };
|
package/lib/codegen-engine.js
CHANGED
|
@@ -236,6 +236,10 @@ function installCodegenPaths (ctx) {
|
|
|
236
236
|
// The boolean engine is the verdict authority for these paths; the
|
|
237
237
|
// final lazy wrapper uses it to skip error construction entirely.
|
|
238
238
|
if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
|
|
239
|
+
// With a preprocess pass the verdict runs it first, for the lazy layer in
|
|
240
|
+
// lib/validator-core.js, which then builds the errors only when they are
|
|
241
|
+
// read, from the document as the pass left it.
|
|
242
|
+
if ((!hasDynRef || _isCodegen) && preprocess && !fusedRemove) { ctx.preVerdict = (data) => { preprocess(data); return jsFn(data) }; ctx.rawVerdict = jsFn; }
|
|
239
243
|
|
|
240
244
|
if (options.abortEarly && jsFn && !hasDynRef) {
|
|
241
245
|
// abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
|
|
@@ -319,7 +323,7 @@ function installCodegenPaths (ctx) {
|
|
|
319
323
|
// deciding again: the lazy layer in lib/validator-core.js has its verdict
|
|
320
324
|
// and used to call validate() to get the errors, which ran the verdict a
|
|
321
325
|
// second time and built a second rejection around it.
|
|
322
|
-
|
|
326
|
+
ctx.rejectBase = onReject;
|
|
323
327
|
// The rich combined function, for a validator whose errors are being read
|
|
324
328
|
// with richErrors on. See _ensureCompiled.
|
|
325
329
|
if (!preprocess) {
|
|
@@ -360,7 +364,7 @@ function installCodegenPaths (ctx) {
|
|
|
360
364
|
this.validate = preprocess
|
|
361
365
|
? (data) => { preprocess(data); return safeCombinedFn(data); }
|
|
362
366
|
: safeCombinedFn;
|
|
363
|
-
|
|
367
|
+
ctx.rejectBase = (data) => _mustReject(safeCombinedFn(data));
|
|
364
368
|
} else {
|
|
365
369
|
this.validate = preprocess
|
|
366
370
|
? (data) => {
|
|
@@ -368,7 +372,7 @@ function installCodegenPaths (ctx) {
|
|
|
368
372
|
return jsFn(data) ? VALID_RESULT : errOnly(data);
|
|
369
373
|
}
|
|
370
374
|
: (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
|
|
371
|
-
|
|
375
|
+
ctx.rejectBase = errOnly;
|
|
372
376
|
}
|
|
373
377
|
}
|
|
374
378
|
// Verbose mode: populate parentSchema, schema and data on each error, the
|
|
@@ -803,18 +807,88 @@ function emitCoercions(node, ov, lines, st, depth) {
|
|
|
803
807
|
}
|
|
804
808
|
st.seen.delete(node);
|
|
805
809
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
810
|
+
// From the closure pass, on the first reference a defaults walk meets: loaded
|
|
811
|
+
// with this module it joined every process's cold start.
|
|
812
|
+
let _localRef = null;
|
|
813
|
+
const localRef = (root, ref) => (_localRef || (_localRef = require('./defaults').localRef))(root, ref);
|
|
814
|
+
// Whether a default is reachable from a schema through the places the pass
|
|
815
|
+
// goes (lib/defaults.js reachesDefault, which this pass is held to): the
|
|
816
|
+
// same walk, with answers kept per schema object for the whole pass, except
|
|
817
|
+
// one that rests on a schema still being decided (a recursive reference). It
|
|
818
|
+
// started over for every property, quadratic in the depth of the schema.
|
|
819
|
+
function reachState(root) { return { root, memo: new Map() }; }
|
|
820
|
+
function hasDefaultsInside(node, rs) { return reachDefault(node, rs, new Set()) === 1; }
|
|
821
|
+
function reachDefault(node, rs, stack) {
|
|
822
|
+
if (!node || typeof node !== 'object') return 0;
|
|
823
|
+
const known = rs.memo.get(node);
|
|
824
|
+
if (known !== undefined) return known;
|
|
825
|
+
if (stack.has(node)) return 2;
|
|
826
|
+
stack.add(node);
|
|
827
|
+
let open = false;
|
|
828
|
+
const check = (n) => { const r = reachDefault(n, rs, stack); if (r === 2) open = true; return r === 1; };
|
|
829
|
+
let yes = false;
|
|
830
|
+
if (node.properties && typeof node.properties === 'object') {
|
|
831
|
+
for (const prop of Object.values(node.properties)) {
|
|
832
|
+
if (prop && typeof prop === 'object' && (prop.default !== undefined || check(prop))) { yes = true; break; }
|
|
833
|
+
}
|
|
812
834
|
}
|
|
813
|
-
|
|
814
|
-
|
|
835
|
+
if (!yes && typeof node.$ref === 'string') yes = check(localRef(rs.root, node.$ref));
|
|
836
|
+
if (!yes) for (const list of [node.allOf, node.prefixItems]) { if (Array.isArray(list) && list.some(check)) { yes = true; break; } }
|
|
837
|
+
if (!yes) for (const one of [node.items, node.additionalProperties]) { if (check(one)) { yes = true; break; } }
|
|
838
|
+
if (!yes) for (const map of [node.patternProperties, node.dependentSchemas]) { if (map && typeof map === 'object' && Object.keys(map).some((k) => check(map[k]))) { yes = true; break; } }
|
|
839
|
+
stack.delete(node);
|
|
840
|
+
if (yes) { rs.memo.set(node, 1); return 1; }
|
|
841
|
+
if (!open) { rs.memo.set(node, 0); return 0; }
|
|
842
|
+
return 2;
|
|
843
|
+
}
|
|
844
|
+
// Which keys `additionalProperties` covers depends on the patterns of
|
|
845
|
+
// `patternProperties`, which this pass does not test (a RegExp built on
|
|
846
|
+
// every call would cost more than the pass): a schema whose defaults depend
|
|
847
|
+
// on one is left to the closure pass.
|
|
848
|
+
function defaultsNeedPatterns(node, rs) {
|
|
849
|
+
const pp = node.patternProperties;
|
|
850
|
+
if (!pp || typeof pp !== 'object' || Object.keys(pp).length === 0) return false;
|
|
851
|
+
for (const k of Object.keys(pp)) if (hasDefaultsInside(pp[k], rs)) return true;
|
|
852
|
+
return hasDefaultsInside(node.additionalProperties, rs);
|
|
853
|
+
}
|
|
854
|
+
// The defaults of `schema` filled into the value of `expr`, when it holds an
|
|
855
|
+
// object (an array included), as the closure pass fills them.
|
|
856
|
+
function emitDefaultsAt(schema, expr, lines, st) {
|
|
857
|
+
const n = '_d' + st.n++;
|
|
858
|
+
lines.push(`{const ${n}=${expr};if(typeof ${n}==='object'&&${n}!==null){`);
|
|
859
|
+
emitDefaults(schema, n, lines, st);
|
|
860
|
+
lines.push('}}');
|
|
861
|
+
}
|
|
862
|
+
// A pass that would expand to more than this many lines (references written
|
|
863
|
+
// out again at every use) is left to the closure pass.
|
|
864
|
+
const DEFAULTS_MAX_LINES = 4000;
|
|
865
|
+
// How many schema objects emitDefaults would write out, references expanded
|
|
866
|
+
// at every use, or Infinity where it would decline (a cycle, or defaults
|
|
867
|
+
// under patternProperties). Each writes at least one line, so a count over
|
|
868
|
+
// DEFAULTS_MAX_LINES declines where emitDefaults would, before writing
|
|
869
|
+
// anything: SARIF's schema wrote 4000 lines in 2.7 ms and threw them away.
|
|
870
|
+
function defaultsVisits(node, st, stack) {
|
|
871
|
+
const known = st.visits.get(node);
|
|
872
|
+
if (known !== undefined) return known;
|
|
873
|
+
if (stack.has(node)) return Infinity;
|
|
874
|
+
stack.add(node);
|
|
875
|
+
let n = 1;
|
|
876
|
+
const add = (x) => { if (n <= DEFAULTS_MAX_LINES && hasDefaultsInside(x, st.reach)) n += defaultsVisits(x, st, stack); };
|
|
877
|
+
if (defaultsNeedPatterns(node, st.reach)) n = Infinity;
|
|
878
|
+
for (const prop of Object.values(node.properties || {})) if (prop && typeof prop === 'object') add(prop);
|
|
879
|
+
if (Array.isArray(node.prefixItems)) node.prefixItems.forEach(add);
|
|
880
|
+
add(node.items);
|
|
881
|
+
add(node.additionalProperties);
|
|
882
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') Object.values(node.dependentSchemas).forEach(add);
|
|
883
|
+
if (typeof node.$ref === 'string') add(localRef(st.root, node.$ref));
|
|
884
|
+
if (Array.isArray(node.allOf)) node.allOf.forEach(add);
|
|
885
|
+
stack.delete(node);
|
|
886
|
+
if (n > DEFAULTS_MAX_LINES) n = Infinity;
|
|
887
|
+
st.visits.set(node, n);
|
|
888
|
+
return n;
|
|
815
889
|
}
|
|
816
890
|
function emitDefaults(node, ov, lines, st) {
|
|
817
|
-
if (st.seen.has(node)) { st.cycle = true; return; }
|
|
891
|
+
if (st.seen.has(node) || lines.length > DEFAULTS_MAX_LINES) { st.cycle = true; return; }
|
|
818
892
|
st.seen.add(node);
|
|
819
893
|
// A default is written only where the key is not an own property, and the
|
|
820
894
|
// own-property test used to be Object.hasOwn for every default on every
|
|
@@ -846,7 +920,7 @@ function emitDefaults(node, ov, lines, st) {
|
|
|
846
920
|
// closure pass walks it. Object.prototype's own properties are functions
|
|
847
921
|
// apart from the __proto__ accessor, so on a plain object a read that
|
|
848
922
|
// gives an object is an own property for every other key.
|
|
849
|
-
if (
|
|
923
|
+
if (hasDefaultsInside(prop, st.reach)) {
|
|
850
924
|
const n = '_d' + st.n++;
|
|
851
925
|
const own = inherited ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
|
|
852
926
|
lines.push(`{const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
|
|
@@ -854,6 +928,56 @@ function emitDefaults(node, ov, lines, st) {
|
|
|
854
928
|
lines.push('}}');
|
|
855
929
|
}
|
|
856
930
|
}
|
|
931
|
+
if (defaultsNeedPatterns(node, st.reach)) { st.cycle = true; st.seen.delete(node); return; }
|
|
932
|
+
// Elements of an array: `prefixItems` by position, `items` after them.
|
|
933
|
+
const has = (x) => hasDefaultsInside(x, st.reach);
|
|
934
|
+
const tuple = Array.isArray(node.prefixItems) ? node.prefixItems : [];
|
|
935
|
+
const tupleHas = tuple.map(has);
|
|
936
|
+
const itemsHas = has(node.items);
|
|
937
|
+
if (tupleHas.some(Boolean) || itemsHas) {
|
|
938
|
+
lines.push(`if(Array.isArray(${ov})){`);
|
|
939
|
+
for (let i = 0; i < tuple.length; i++) {
|
|
940
|
+
if (!tupleHas[i]) continue;
|
|
941
|
+
lines.push(`if(${ov}.length>${i})`);
|
|
942
|
+
emitDefaultsAt(tuple[i], `${ov}[${i}]`, lines, st);
|
|
943
|
+
}
|
|
944
|
+
if (itemsHas) {
|
|
945
|
+
const i = '_i' + st.n++;
|
|
946
|
+
lines.push(`for(let ${i}=${tuple.length};${i}<${ov}.length;${i}++)`);
|
|
947
|
+
emitDefaultsAt(node.items, `${ov}[${i}]`, lines, st);
|
|
948
|
+
}
|
|
949
|
+
lines.push('}');
|
|
950
|
+
}
|
|
951
|
+
// Keys of an object that `properties` does not name (no patterns here,
|
|
952
|
+
// see defaultsNeedPatterns).
|
|
953
|
+
if (has(node.additionalProperties)) {
|
|
954
|
+
const k = '_k' + st.n++;
|
|
955
|
+
const declared = Object.keys(node.properties || {});
|
|
956
|
+
lines.push(`if(!Array.isArray(${ov}))for(const ${k} of Object.keys(${ov})){`);
|
|
957
|
+
if (declared.length) lines.push(`if(${declared.map((d) => `${k}===${JSON.stringify(d)}`).join('||')})continue;`);
|
|
958
|
+
emitDefaultsAt(node.additionalProperties, `${ov}[${k}]`, lines, st);
|
|
959
|
+
lines.push('}');
|
|
960
|
+
}
|
|
961
|
+
// A schema for the same object, for each key present.
|
|
962
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
|
|
963
|
+
for (const key of Object.keys(node.dependentSchemas)) {
|
|
964
|
+
const dep = node.dependentSchemas[key];
|
|
965
|
+
if (!has(dep)) continue;
|
|
966
|
+
lines.push(`if(!Array.isArray(${ov})&&Object.hasOwn(${ov},${JSON.stringify(key)})){`);
|
|
967
|
+
emitDefaults(dep, ov, lines, st);
|
|
968
|
+
lines.push('}');
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// The schemas applied to the same value: a `$ref` into the same document,
|
|
972
|
+
// written out here (one back into what is being written is a cycle, left
|
|
973
|
+
// to the closure pass), then each `allOf` member, in the closure pass's order.
|
|
974
|
+
if (typeof node.$ref === 'string') {
|
|
975
|
+
const target = localRef(st.root, node.$ref);
|
|
976
|
+
if (target && hasDefaultsInside(target, st.reach)) emitDefaults(target, ov, lines, st);
|
|
977
|
+
}
|
|
978
|
+
if (Array.isArray(node.allOf)) {
|
|
979
|
+
for (const sub of node.allOf) if (hasDefaultsInside(sub, st.reach)) emitDefaults(sub, ov, lines, st);
|
|
980
|
+
}
|
|
857
981
|
st.seen.delete(node);
|
|
858
982
|
}
|
|
859
983
|
|
|
@@ -861,7 +985,11 @@ function emitDefaults(node, ov, lines, st) {
|
|
|
861
985
|
// the closure passes must take the schema. A module written ahead of time
|
|
862
986
|
// embeds it as applyDefaults (lib/aot-impl.js), so it runs the runtime's code.
|
|
863
987
|
function preprocessSource(schema, options) {
|
|
864
|
-
if (typeof schema !== 'object' || schema === null
|
|
988
|
+
if (typeof schema !== 'object' || schema === null) return null;
|
|
989
|
+
// Defaults reach through `$ref` and `allOf` from the root too; the other
|
|
990
|
+
// passes start at `properties`.
|
|
991
|
+
const reach = reachState(schema);
|
|
992
|
+
if (!schema.properties && !(options.useDefaults !== false && hasDefaultsInside(schema, reach))) return null;
|
|
865
993
|
const lines = [];
|
|
866
994
|
|
|
867
995
|
// removeAdditional: strip unknown keys at every level the schema describes,
|
|
@@ -880,11 +1008,14 @@ function preprocessSource(schema, options) {
|
|
|
880
1008
|
// used to stop at the top-level properties. A schema object that contains
|
|
881
1009
|
// itself declines here, and the closure passes, which carry a guard for it,
|
|
882
1010
|
// take the schema.
|
|
883
|
-
const st = { n: 0, seen: new Set(), cycle: false, arrayMode: options.coerceTypes === 'array', memo: new Map() };
|
|
1011
|
+
const st = { n: 0, seen: new Set(), cycle: false, arrayMode: options.coerceTypes === 'array', memo: new Map(), root: schema, reach, visits: new Map() };
|
|
884
1012
|
if (options.coerceTypes) emitCoercions(schema, 'd', lines, st, 0);
|
|
885
1013
|
// hasDefaultsInside answers without emitting; most schemas have no default,
|
|
886
1014
|
// and the emitting walk allocates for every property it visits.
|
|
887
|
-
if (options.useDefaults !== false && hasDefaultsInside(schema,
|
|
1015
|
+
if (options.useDefaults !== false && hasDefaultsInside(schema, reach)) {
|
|
1016
|
+
if (defaultsVisits(schema, st, new Set()) > DEFAULTS_MAX_LINES) return null;
|
|
1017
|
+
emitDefaults(schema, 'd', lines, st);
|
|
1018
|
+
}
|
|
888
1019
|
if (st.cycle) return null;
|
|
889
1020
|
|
|
890
1021
|
if (lines.length === 0) return null;
|
package/lib/defaults.js
CHANGED
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
// The closure pass that fills `default` values, shared by the validator core
|
|
4
4
|
// and by the wrapper around a compiled module, so both fill defaults the same
|
|
5
|
-
// way. The generated pass in
|
|
5
|
+
// way. The generated pass in lib/codegen-engine.js is held to this one by
|
|
6
6
|
// tests/test_nested_defaults_engines.js.
|
|
7
|
+
//
|
|
8
|
+
// A default is filled in every schema applied to the value at that place, as
|
|
9
|
+
// ajv's `useDefaults` fills one: the schemas under `properties`, through a
|
|
10
|
+
// `$ref` into the same document (recursive ones included), each `allOf`
|
|
11
|
+
// member, `items` for every element and `prefixItems` by position,
|
|
12
|
+
// `patternProperties` for the keys they match, `additionalProperties` for the
|
|
13
|
+
// rest, and `dependentSchemas` for the keys present. The pass stopped at
|
|
14
|
+
// `properties`, so a default behind a reference or in an array element was
|
|
15
|
+
// not filled, and a `required` property with one was rejected where ajv
|
|
16
|
+
// accepts it. `anyOf`, `oneOf` and `not` fill nothing, as with ajv;
|
|
17
|
+
// `if`/`then`/`else` fill nothing here, since which branch applies is only
|
|
18
|
+
// known once the value has been checked.
|
|
7
19
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
20
|
+
// Below a value nested this deep the pass stops: data that contains itself,
|
|
21
|
+
// under a recursive schema, would otherwise never end.
|
|
22
|
+
const MAX_DEPTH = 512;
|
|
23
|
+
|
|
24
|
+
// Build the pass for a schema: a function that fills defaults into a value in
|
|
25
|
+
// place (mutates), or null when no default is reachable from the schema.
|
|
10
26
|
function buildDefaultsApplier(schema) {
|
|
11
27
|
if (typeof schema !== "object" || schema === null) return null;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return (data) => {
|
|
16
|
-
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
17
|
-
};
|
|
28
|
+
const path = [];
|
|
29
|
+
const fn = compileNode(schema, schema, new Map(), new Map(), path);
|
|
30
|
+
return fn === null ? null : (data) => { path.length = 0; fn(data, 0, null); };
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
// Write an own property. Plain assignment of a key named `__proto__` does
|
|
@@ -34,55 +47,199 @@ function setOwn(obj, key, val) {
|
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
50
|
+
// The schema a `$ref` names in the same document: `#`, or a JSON pointer
|
|
51
|
+
// after it. Null for anything else (another document, an anchor), which the
|
|
52
|
+
// pass leaves alone. Normalization renames draft-07 `definitions` to `$defs`
|
|
53
|
+
// and leaves the pointers as written, so either name reads as the other.
|
|
54
|
+
function localRef(root, ref) {
|
|
55
|
+
if (typeof ref !== "string" || ref.charCodeAt(0) !== 35) return null;
|
|
56
|
+
if (ref === "#") return root;
|
|
57
|
+
if (ref.charCodeAt(1) !== 47) return null;
|
|
58
|
+
let t = root;
|
|
59
|
+
for (let seg of ref.slice(2).split("/")) {
|
|
60
|
+
if (seg.indexOf("%") >= 0) { try { seg = decodeURIComponent(seg); } catch { return null; } }
|
|
61
|
+
if (seg.indexOf("~") >= 0) seg = seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
62
|
+
if (t === null || typeof t !== "object") return null;
|
|
63
|
+
if (!Object.hasOwn(t, seg)) {
|
|
64
|
+
const alias = seg === "definitions" ? "$defs" : seg === "$defs" ? "definitions" : null;
|
|
65
|
+
if (alias === null || !Object.hasOwn(t, alias)) return null;
|
|
66
|
+
seg = alias;
|
|
67
|
+
}
|
|
68
|
+
t = t[seg];
|
|
69
|
+
}
|
|
70
|
+
return t !== null && typeof t === "object" ? t : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Whether a default is reachable from a schema, through the same places the
|
|
74
|
+
// pass goes: 1 yes, 0 no, 2 not known yet because the walk came back to a
|
|
75
|
+
// schema it is still deciding (a recursive reference). Answers are kept per
|
|
76
|
+
// schema object in `memo`, except one that rests on an undecided schema,
|
|
77
|
+
// which is worked out again when asked; the walk used to start over for every
|
|
78
|
+
// schema object it compiled, quadratic in the depth of the schema.
|
|
79
|
+
function reachesDefault(node, root, memo, stack) {
|
|
80
|
+
if (node === null || typeof node !== "object") return 0;
|
|
81
|
+
const known = memo.get(node);
|
|
82
|
+
if (known !== undefined) return known;
|
|
83
|
+
if (stack.has(node)) return 2;
|
|
84
|
+
stack.add(node);
|
|
85
|
+
const props = valuesOf(node.properties);
|
|
86
|
+
let yes = props.some((p) => p && typeof p === "object" && p.default !== undefined);
|
|
87
|
+
let open = false;
|
|
88
|
+
const below = props.concat(localRef(root, node.$ref), listOf(node.allOf), listOf(node.prefixItems), node.items, node.additionalProperties, valuesOf(node.patternProperties), valuesOf(node.dependentSchemas));
|
|
89
|
+
for (let i = 0; !yes && i < below.length; i++) {
|
|
90
|
+
const r = reachesDefault(below[i], root, memo, stack);
|
|
91
|
+
if (r === 1) yes = true;
|
|
92
|
+
else if (r === 2) open = true;
|
|
93
|
+
}
|
|
94
|
+
stack.delete(node);
|
|
95
|
+
if (yes || !open) memo.set(node, yes ? 1 : 0);
|
|
96
|
+
return yes ? 1 : open ? 2 : 0;
|
|
97
|
+
}
|
|
98
|
+
function valuesOf(m) {
|
|
99
|
+
return m && typeof m === "object" && !Array.isArray(m) ? Object.values(m) : [];
|
|
100
|
+
}
|
|
101
|
+
function listOf(a) {
|
|
102
|
+
return Array.isArray(a) ? a : [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A pattern of `patternProperties` as the validator reads one.
|
|
106
|
+
function keyPattern(src) {
|
|
107
|
+
try { return new RegExp(src, "u"); } catch { try { return new RegExp(src); } catch { return null; } }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A fresh copy of a default for each value it fills. `[]`, the most common
|
|
111
|
+
// object default (2916 of the 4130 SARIF's schema fills into one log), skips
|
|
112
|
+
// the JSON round trip.
|
|
113
|
+
function copyOf(v) {
|
|
114
|
+
return typeof v !== "object" || v === null ? v : Array.isArray(v) && v.length === 0 ? [] : JSON.parse(JSON.stringify(v));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The pass for one schema object, built once: its own defaults, then the
|
|
118
|
+
// schemas below its properties, then its `$ref` and its `allOf` members, each
|
|
119
|
+
// applied to the same value. A schema reached again (a recursive reference)
|
|
120
|
+
// takes the function being built for it, so a cycle in the schema is a loop
|
|
121
|
+
// in the calls, bounded by the depth of the data.
|
|
122
|
+
function compileNode(node, root, memo, reach, path) {
|
|
123
|
+
if (node === null || typeof node !== "object") return null;
|
|
124
|
+
if (memo.has(node)) return memo.get(node);
|
|
125
|
+
if (reachesDefault(node, root, reach, new Set()) !== 1) { memo.set(node, null); return null; }
|
|
126
|
+
let body = null;
|
|
127
|
+
// `path`: the schemas applied to the enclosing values, outermost first.
|
|
128
|
+
// `made`: inside a value the pass itself made from a default, the same as a
|
|
129
|
+
// set, and null elsewhere. There a schema already applied to an enclosing
|
|
130
|
+
// value is not applied again: one that refers back to an enclosing schema,
|
|
131
|
+
// with an object default, would otherwise make a value, fill it, make the
|
|
132
|
+
// next one inside it, and never stop. ajv throws a RangeError on such a
|
|
133
|
+
// schema, and Renovate's took this pass to the heap limit.
|
|
134
|
+
const fn = (data, depth, made) => {
|
|
135
|
+
if (depth > MAX_DEPTH) return;
|
|
136
|
+
if (made !== null) {
|
|
137
|
+
if (made.has(node)) return;
|
|
138
|
+
made.add(node);
|
|
139
|
+
body(data, depth, made);
|
|
140
|
+
made.delete(node);
|
|
141
|
+
return;
|
|
80
142
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
143
|
+
path.push(node);
|
|
144
|
+
body(data, depth, null);
|
|
145
|
+
path.pop();
|
|
146
|
+
};
|
|
147
|
+
memo.set(node, fn);
|
|
148
|
+
|
|
149
|
+
const own = [];
|
|
150
|
+
const below = [];
|
|
151
|
+
const props = node.properties;
|
|
152
|
+
if (props && typeof props === "object") {
|
|
153
|
+
for (const key of Object.keys(props)) {
|
|
154
|
+
const p = props[key];
|
|
155
|
+
if (!p || typeof p !== "object") continue;
|
|
156
|
+
if (p.default !== undefined) own.push([key, p.default]);
|
|
157
|
+
const child = compileNode(p, root, memo, reach, path);
|
|
158
|
+
if (child !== null) below.push([key, child]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Arrays: every element under `items`, the elements `prefixItems` names by
|
|
162
|
+
// position (then `items` covers only the ones after them).
|
|
163
|
+
const tuple = Array.isArray(node.prefixItems) ? node.prefixItems.map((s) => compileNode(s, root, memo, reach, path)) : [];
|
|
164
|
+
const each = compileNode(node.items, root, memo, reach, path);
|
|
165
|
+
// Objects: keys a pattern matches, and the keys neither `properties` nor a
|
|
166
|
+
// pattern names; and a schema for each key present.
|
|
167
|
+
const patterns = [];
|
|
168
|
+
if (node.patternProperties && typeof node.patternProperties === "object") {
|
|
169
|
+
for (const src of Object.keys(node.patternProperties)) {
|
|
170
|
+
const re = keyPattern(src);
|
|
171
|
+
const t = compileNode(node.patternProperties[src], root, memo, reach, path);
|
|
172
|
+
if (re === null) continue;
|
|
173
|
+
patterns.push([re, t]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const extra = compileNode(node.additionalProperties, root, memo, reach, path);
|
|
177
|
+
const declared = props && typeof props === "object" ? props : null;
|
|
178
|
+
const dependent = [];
|
|
179
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === "object") {
|
|
180
|
+
for (const key of Object.keys(node.dependentSchemas)) {
|
|
181
|
+
const t = compileNode(node.dependentSchemas[key], root, memo, reach, path);
|
|
182
|
+
if (t !== null) dependent.push([key, t]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const keyed = extra !== null || patterns.some((p) => p[1] !== null);
|
|
186
|
+
|
|
187
|
+
const also = [];
|
|
188
|
+
if (typeof node.$ref === "string") {
|
|
189
|
+
const t = compileNode(localRef(root, node.$ref), root, memo, reach, path);
|
|
190
|
+
if (t !== null) also.push(t);
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(node.allOf)) {
|
|
193
|
+
for (const s of node.allOf) {
|
|
194
|
+
const t = compileNode(s, root, memo, reach, path);
|
|
195
|
+
if (t !== null) also.push(t);
|
|
84
196
|
}
|
|
85
197
|
}
|
|
198
|
+
|
|
199
|
+
body = (data, depth, made) => {
|
|
200
|
+
let fresh = null;
|
|
201
|
+
if (typeof data === "object" && data !== null) {
|
|
202
|
+
for (let i = 0; i < own.length; i++) {
|
|
203
|
+
const key = own[i][0];
|
|
204
|
+
if (!Object.hasOwn(data, key)) {
|
|
205
|
+
const v = copyOf(own[i][1]);
|
|
206
|
+
setOwn(data, key, v);
|
|
207
|
+
if (made === null && typeof v === "object" && v !== null) (fresh || (fresh = [])).push(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (let i = 0; i < below.length; i++) {
|
|
211
|
+
// Own keys only. `data[key]` for an inherited name walks the
|
|
212
|
+
// prototype chain: a property named `__proto__` that the value does
|
|
213
|
+
// not carry resolved to Object.prototype, and the defaults below it
|
|
214
|
+
// were written onto it, for every object in the realm.
|
|
215
|
+
const key = below[i][0];
|
|
216
|
+
if (Object.hasOwn(data, key)) below[i][1](data[key], depth + 1, fresh !== null && fresh.includes(key) ? new Set(path) : made);
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(data)) {
|
|
219
|
+
for (let i = 0; i < data.length; i++) {
|
|
220
|
+
const t = i < tuple.length ? tuple[i] : each;
|
|
221
|
+
if (t !== null && t !== undefined) t(data[i], depth + 1, made);
|
|
222
|
+
}
|
|
223
|
+
} else if (keyed) {
|
|
224
|
+
for (const key of Object.keys(data)) {
|
|
225
|
+
let matched = false;
|
|
226
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
227
|
+
if (patterns[i][0].test(key)) { matched = true; if (patterns[i][1] !== null) patterns[i][1](data[key], depth + 1, made); }
|
|
228
|
+
}
|
|
229
|
+
if (extra !== null && !matched && !(declared !== null && Object.hasOwn(declared, key))) extra(data[key], depth + 1, made);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(data)) {
|
|
233
|
+
for (let i = 0; i < dependent.length; i++) if (Object.hasOwn(data, dependent[i][0])) dependent[i][1](data, depth + 1, made);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (let i = 0; i < also.length; i++) also[i](data, depth + 1, made);
|
|
237
|
+
};
|
|
238
|
+
return fn;
|
|
86
239
|
}
|
|
87
240
|
|
|
88
|
-
module
|
|
241
|
+
// The functions a module written ahead of time embeds as its applyDefaults
|
|
242
|
+
// where the generated pass declines a schema (lib/aot-impl.js).
|
|
243
|
+
const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
|
|
244
|
+
|
|
245
|
+
module.exports = { buildDefaultsApplier, setOwn, localRef, MAX_DEPTH, EMBED };
|
package/lib/error-codes.js
CHANGED
|
@@ -136,6 +136,7 @@ const NATIVE_KEYWORDS = [
|
|
|
136
136
|
'not', // 25 not_failed
|
|
137
137
|
'$ref', // 26 ref_not_found
|
|
138
138
|
'if', // 27 if_then_else_failed
|
|
139
|
+
'__depth__', // 28 depth_exceeded
|
|
139
140
|
]
|
|
140
141
|
|
|
141
142
|
// Keyword and public code for a native ordinal, or null when the ordinal is
|
|
@@ -147,6 +148,11 @@ function fromNative (ordinal, format) {
|
|
|
147
148
|
// codeFor('format') answers with the first format code, which names a
|
|
148
149
|
// specific format. Without knowing which format failed, the generic one is
|
|
149
150
|
// the honest answer.
|
|
151
|
+
// The walker stopped at its depth limit and rejected the document. No
|
|
152
|
+
// public code names that: the APIs that ask the walker route schemas deep or
|
|
153
|
+
// recursive enough to reach it to validate() first (lib/buffer-gate.js), so
|
|
154
|
+
// it is the generic failure.
|
|
155
|
+
if (keyword === '__depth__') return { keyword, code: 'ATA9000' }
|
|
150
156
|
const code = keyword === 'format'
|
|
151
157
|
? (format ? codeFor('format', format) || 'ATA3099' : 'ATA3099')
|
|
152
158
|
: codeFor(keyword) || 'ATA9001'
|
package/lib/formats-source.js
CHANGED
|
@@ -304,4 +304,4 @@ function durationSource (v, isStr) {
|
|
|
304
304
|
const inner = `if(!${_formats().DURATION_RE.toString()}.test(${v}))return false`;
|
|
305
305
|
return isStr ? inner : `if(typeof ${v}==='string'&&${inner.slice(3)}`;
|
|
306
306
|
}
|
|
307
|
-
module.exports = { uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource,
|
|
307
|
+
module.exports = { uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
|
package/lib/interpreter-core.js
CHANGED
|
@@ -903,7 +903,10 @@ class Interpreter {
|
|
|
903
903
|
_fastRoot() {
|
|
904
904
|
if (this._fast === undefined) {
|
|
905
905
|
const compile = planCompiler();
|
|
906
|
-
|
|
906
|
+
// Still on its way (the browser build loads it after the page's first
|
|
907
|
+
// checks, lib/interpreter.browser.js): the plain walk answers, and the
|
|
908
|
+
// next call asks again.
|
|
909
|
+
if (compile === null) { if (planPending) return null; this._fast = null; }
|
|
907
910
|
else {
|
|
908
911
|
try { this._fast = compile(this); } catch (e) {
|
|
909
912
|
// The plain walk answers instead, which tests cannot tell apart;
|
|
@@ -1530,9 +1533,12 @@ function err(code, keyword, instancePath, schemaPath, params, message) {
|
|
|
1530
1533
|
const TIER = 1000;
|
|
1531
1534
|
let loadPlanCompiler = null;
|
|
1532
1535
|
let compileInterpreter = null;
|
|
1533
|
-
|
|
1536
|
+
// True while a plan compiler is being loaded and none is set yet.
|
|
1537
|
+
let planPending = false;
|
|
1538
|
+
function setPlanCompiler(load, pending) {
|
|
1534
1539
|
loadPlanCompiler = load;
|
|
1535
1540
|
compileInterpreter = null;
|
|
1541
|
+
planPending = load === null && pending === true;
|
|
1536
1542
|
}
|
|
1537
1543
|
function planCompiler() {
|
|
1538
1544
|
if (compileInterpreter === null && loadPlanCompiler !== null) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The interpreted engine for the browser build (the package.json browser
|
|
4
|
+
// field maps lib/interpreter.js here). The plan compiler, the closure tree
|
|
5
|
+
// that makes the engine fast on a schema checked many times, is not in the
|
|
6
|
+
// page's first load: it comes through the dynamic import below, which
|
|
7
|
+
// bundlers split into a chunk of its own, once the page is idle after its
|
|
8
|
+
// first validator. Until then the plain walk answers, as it does for every
|
|
9
|
+
// schema's first thousand calls (TIER in lib/interpreter-core.js), with the
|
|
10
|
+
// same answers. Without a bundler that splits, the chunk is inlined and
|
|
11
|
+
// nothing changes.
|
|
12
|
+
const core = require('./interpreter-core');
|
|
13
|
+
|
|
14
|
+
core._setPlanCompiler(null, true);
|
|
15
|
+
let asked = false;
|
|
16
|
+
const idle = (f) => {
|
|
17
|
+
if (typeof requestIdleCallback === 'function') return requestIdleCallback(f, { timeout: 2000 });
|
|
18
|
+
const t = setTimeout(f, 200);
|
|
19
|
+
if (t && typeof t.unref === 'function') t.unref();
|
|
20
|
+
};
|
|
21
|
+
function load() {
|
|
22
|
+
import('./plan-compiler.js').then((m) => {
|
|
23
|
+
const mod = m && m.install ? m : m.default;
|
|
24
|
+
core._setPlanCompiler(() => mod.install);
|
|
25
|
+
}, () => core._setPlanCompiler(null));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The first engine built asks for it, when the page is next idle.
|
|
29
|
+
const create = core.createInterpreter;
|
|
30
|
+
core.createInterpreter = (schema, opts) => {
|
|
31
|
+
if (!asked) { asked = true; idle(load); }
|
|
32
|
+
return create(schema, opts);
|
|
33
|
+
};
|
|
34
|
+
module.exports = core;
|
package/lib/schema-strip.js
CHANGED
|
@@ -63,24 +63,24 @@ function refsIntoDropped(node, seen) {
|
|
|
63
63
|
|
|
64
64
|
// keepTitle: the node is a branch of anyOf or oneOf, whose title names the
|
|
65
65
|
// branch in the collapsed error (params.closestName).
|
|
66
|
-
function stripSchema(node, keepTitle) {
|
|
66
|
+
function stripSchema(node, keepTitle, keepDefault) {
|
|
67
67
|
if (node === null || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
68
68
|
const out = {};
|
|
69
69
|
for (const k of Object.keys(node)) {
|
|
70
|
-
if (dropped(k) && !(keepTitle === true && k === 'title')) continue;
|
|
70
|
+
if (dropped(k) && !(keepTitle === true && k === 'title') && !(keepDefault === true && k === 'default')) continue;
|
|
71
71
|
const v = node[k];
|
|
72
72
|
if (SINGLE.has(k)) {
|
|
73
|
-
setOwn(out, k, Array.isArray(v) ? v.map((x) => stripSchema(x, false)) : stripSchema(v, false));
|
|
73
|
+
setOwn(out, k, Array.isArray(v) ? v.map((x) => stripSchema(x, false, keepDefault)) : stripSchema(v, false, keepDefault));
|
|
74
74
|
} else if (MAPS.has(k) && v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
75
75
|
const m = {};
|
|
76
|
-
for (const name of Object.keys(v)) setOwn(m, name, stripSchema(v[name], false));
|
|
76
|
+
for (const name of Object.keys(v)) setOwn(m, name, stripSchema(v[name], false, keepDefault));
|
|
77
77
|
setOwn(out, k, m);
|
|
78
78
|
} else if (LISTS.has(k) && Array.isArray(v)) {
|
|
79
79
|
const branches = k === 'anyOf' || k === 'oneOf';
|
|
80
|
-
setOwn(out, k, v.map((x) => stripSchema(x, branches)));
|
|
80
|
+
setOwn(out, k, v.map((x) => stripSchema(x, branches, keepDefault)));
|
|
81
81
|
} else if (k === 'dependencies' && v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
82
82
|
const m = {};
|
|
83
|
-
for (const name of Object.keys(v)) setOwn(m, name, Array.isArray(v[name]) ? v[name] : stripSchema(v[name], false));
|
|
83
|
+
for (const name of Object.keys(v)) setOwn(m, name, Array.isArray(v[name]) ? v[name] : stripSchema(v[name], false, keepDefault));
|
|
84
84
|
setOwn(out, k, m);
|
|
85
85
|
} else if (k === 'propertyDependencies' && v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
86
86
|
const m = {};
|
|
@@ -88,7 +88,7 @@ function stripSchema(node, keepTitle) {
|
|
|
88
88
|
const inner = v[name];
|
|
89
89
|
if (inner === null || typeof inner !== 'object' || Array.isArray(inner)) { setOwn(m, name, inner); continue; }
|
|
90
90
|
const mi = {};
|
|
91
|
-
for (const val of Object.keys(inner)) setOwn(mi, val, stripSchema(inner[val], false));
|
|
91
|
+
for (const val of Object.keys(inner)) setOwn(mi, val, stripSchema(inner[val], false, keepDefault));
|
|
92
92
|
setOwn(m, name, mi);
|
|
93
93
|
}
|
|
94
94
|
setOwn(out, k, m);
|
|
@@ -112,4 +112,11 @@ function stripAnnotations(schema) {
|
|
|
112
112
|
return out;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
module
|
|
115
|
+
// The same copy with every `default` kept, for a module that fills defaults
|
|
116
|
+
// from its embedded schema (lib/aot-impl.js).
|
|
117
|
+
function stripAnnotationsKeepingDefaults(schema) {
|
|
118
|
+
if (schema === null || typeof schema !== 'object') return schema;
|
|
119
|
+
return refsIntoDropped(schema, new Set()) ? schema : stripSchema(schema, false, true);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { stripAnnotations, stripAnnotationsKeepingDefaults };
|
package/lib/validator-core.js
CHANGED
|
@@ -1098,22 +1098,22 @@ class Validator {
|
|
|
1098
1098
|
// routes, and generating and compiling the pass again for each was most of
|
|
1099
1099
|
// what a repeated schema cost to set up.
|
|
1100
1100
|
let preprocess = null;
|
|
1101
|
+
// A root schema whose text has no `default` has none to fill, and neither
|
|
1102
|
+
// pass looks for one. One reached through a reference into another
|
|
1103
|
+
// document is walked, since its defaults are not in this text.
|
|
1104
|
+
const mayHaveDefaults = preprocessSchema !== schemaObj || this._schemaStr.includes('"default"');
|
|
1101
1105
|
if (!this._interpretOnly && _codegen !== null) {
|
|
1102
1106
|
const cacheable = !this._userFormats && !this._usesKeywords;
|
|
1103
1107
|
const ppKey = cacheable ? mapKey + '\u0000' + String(options.coerceTypes) + '|' + (options.useDefaults !== false) + '|' + String(options.removeAdditional) : null;
|
|
1104
1108
|
const hit = ppKey !== null ? _preprocessCache.get(ppKey) : undefined;
|
|
1105
1109
|
if (hit !== undefined) preprocess = hit;
|
|
1106
1110
|
else {
|
|
1107
|
-
preprocess = _codegen.buildPreprocess(preprocessSchema, options) || null;
|
|
1111
|
+
preprocess = _codegen.buildPreprocess(preprocessSchema, mayHaveDefaults || options.useDefaults === false ? options : { ...options, useDefaults: false }) || null;
|
|
1108
1112
|
if (ppKey !== null && preprocess !== null) _boundedSet(_preprocessCache, ppKey, preprocess);
|
|
1109
1113
|
}
|
|
1110
1114
|
}
|
|
1111
1115
|
if (!preprocess) {
|
|
1112
|
-
//
|
|
1113
|
-
// defaults module then stays unloaded. One reached through a reference
|
|
1114
|
-
// into another document is walked, since its defaults are not in this
|
|
1115
|
-
// text.
|
|
1116
|
-
const mayHaveDefaults = preprocessSchema !== schemaObj || this._schemaStr.includes('"default"');
|
|
1116
|
+
// Without a `default` in the text the defaults module stays unloaded.
|
|
1117
1117
|
const applyDefaults = options.useDefaults === false || !mayHaveDefaults ? null : buildDefaultsApplier(preprocessSchema);
|
|
1118
1118
|
const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
|
|
1119
1119
|
const applyRemove = options.removeAdditional
|
|
@@ -1200,11 +1200,11 @@ class Validator {
|
|
|
1200
1200
|
// locals and on this object rather than on the instance: every call reads
|
|
1201
1201
|
// `validate` off the instance, and three more own properties made that
|
|
1202
1202
|
// read slower across the suite's validators.
|
|
1203
|
-
let _rejectBase = null, _rejectPresented = null, _rejectPlain = false, _oneShotRich = null, _richBuilder = null, _oneShotPlain = false, _isCleanShape = null, _onePassOf = null, _isCombined = null;
|
|
1203
|
+
let _rejectBase = null, _rejectPresented = null, _rejectPlain = false, _oneShotRich = null, _richBuilder = null, _oneShotPlain = false, _isCleanShape = null, _onePassOf = null, _isCombined = null, _preVerdict = null, _rawVerdict = null;
|
|
1204
1204
|
if (jsFn) {
|
|
1205
1205
|
const _cg = {
|
|
1206
1206
|
jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr,
|
|
1207
|
-
combined: () => jsCombinedFn, err: () => jsErrFn, rejectBase: null, richBuilder: null, oneShotPlain: false, isCleanShape: null, isCombined: null, onePassOf: null,
|
|
1207
|
+
combined: () => jsCombinedFn, err: () => jsErrFn, rejectBase: null, richBuilder: null, oneShotPlain: false, isCleanShape: null, isCombined: null, onePassOf: null, preVerdict: null, rawVerdict: null,
|
|
1208
1208
|
// The combined function with errors built enriched where they happen,
|
|
1209
1209
|
// for a validator whose errors are read with richErrors on and no
|
|
1210
1210
|
// schema source to attach frames from (see makeRich).
|
|
@@ -1220,6 +1220,8 @@ class Validator {
|
|
|
1220
1220
|
_oneShotPlain = _cg.oneShotPlain;
|
|
1221
1221
|
_isCleanShape = _cg.isCleanShape;
|
|
1222
1222
|
_onePassOf = _cg.onePassOf;
|
|
1223
|
+
_preVerdict = _cg.preVerdict;
|
|
1224
|
+
_rawVerdict = _cg.rawVerdict;
|
|
1223
1225
|
_isCombined = _cg.isCombined;
|
|
1224
1226
|
} else {
|
|
1225
1227
|
// No JS codegen: the interpreted engine, with or without the addon. The
|
|
@@ -1237,6 +1239,7 @@ class Validator {
|
|
|
1237
1239
|
// run the keyword twice, the second time on what the first left.
|
|
1238
1240
|
const once = interp.dataContext === true;
|
|
1239
1241
|
if (!preprocess && !once) this._fastVerdict = (d) => interp.isValid(d);
|
|
1242
|
+
if (preprocess && !once) _preVerdict = (d) => { preprocess(d); return interp.isValid(d); };
|
|
1240
1243
|
// abortEarly is a documented contract, not a property of whichever engine
|
|
1241
1244
|
// answered: it promises the frozen ATA9000 stub instead of a detailed
|
|
1242
1245
|
// error, so code that branches on it has to behave the same with and
|
|
@@ -1251,7 +1254,7 @@ class Validator {
|
|
|
1251
1254
|
: (data) => interp.validate(data));
|
|
1252
1255
|
this.validate = this._verbose ? _verboseWrap(run, schemaObj) : run;
|
|
1253
1256
|
// Reading errors runs the interpreter once, directly; see _rejectBase.
|
|
1254
|
-
if (!
|
|
1257
|
+
if (!options.abortEarly && !this._verbose && !once) _rejectBase = (data) => interp.validate(data);
|
|
1255
1258
|
_bindVerdict(this, this._fastVerdict
|
|
1256
1259
|
? this._fastVerdict
|
|
1257
1260
|
: (data) => run(data).valid);
|
|
@@ -1442,10 +1445,14 @@ class Validator {
|
|
|
1442
1445
|
// engine and materializes `errors` through a getter on first access.
|
|
1443
1446
|
// A caller that only reads `.valid`, which is every gateway check and
|
|
1444
1447
|
// every benchmark, skips error construction entirely; a caller that
|
|
1445
|
-
// reads `.errors` pays once and the result is cached.
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1448
|
+
// reads `.errors` pays once and the result is cached. A schema that
|
|
1449
|
+
// coerces or defaults takes it too, with a verdict that runs the
|
|
1450
|
+
// preprocess pass first; the errors are then built from the document as
|
|
1451
|
+
// the pass left it. It built them on every rejection, read or not, which
|
|
1452
|
+
// on a large schema was 25 ms of a first validate() spent ordering
|
|
1453
|
+
// errors nobody asked for. Skipped under abortEarly (already a frozen
|
|
1454
|
+
// stub), for $dynamicRef (the boolean engine is not the authority
|
|
1455
|
+
// there), and with removeAdditional's fused pass.
|
|
1449
1456
|
// A check registered through _extendValidate joins here when it can: the
|
|
1450
1457
|
// verdict comes from the generated function with the check compiled in,
|
|
1451
1458
|
// and the check's own errors are appended when somebody reads them. A
|
|
@@ -1453,7 +1460,8 @@ class Validator {
|
|
|
1453
1460
|
// is not installed, the end of this method wraps validate() instead.
|
|
1454
1461
|
const _vx = this._validateTail !== null ? this._validateTail() : null;
|
|
1455
1462
|
let _vxApplied = false;
|
|
1456
|
-
|
|
1463
|
+
const _verdict0 = preprocess ? _preVerdict : this._fastVerdict;
|
|
1464
|
+
if (_verdict0 && !options.abortEarly && this.validate) {
|
|
1457
1465
|
// The errors of a document the verdict rejected come from the rejection
|
|
1458
1466
|
// path directly, not from validate() deciding a second time. Not with a
|
|
1459
1467
|
// check from _extendValidate: that check can reject what the schema
|
|
@@ -1461,7 +1469,7 @@ class Validator {
|
|
|
1461
1469
|
// Not with removeAdditional's fused pass either, which edits the
|
|
1462
1470
|
// document before the errors are read.
|
|
1463
1471
|
const _full = (_vx === null && !fusedRemove && _rejectPresented) ? _rejectPresented : this.validate;
|
|
1464
|
-
const _fast = _vx ? _fuseTail(
|
|
1472
|
+
const _fast = _vx ? _fuseTail(_verdict0, _vx.check) : _verdict0;
|
|
1465
1473
|
const _extra = _vx ? _vx.errors : null;
|
|
1466
1474
|
_vxApplied = true;
|
|
1467
1475
|
const EMPTY_ERRORS = Object.freeze([]);
|
|
@@ -1553,13 +1561,16 @@ class Validator {
|
|
|
1553
1561
|
// the caller and the checks, and allocation sites of its own. Compiled
|
|
1554
1562
|
// only once the validator is in use, as the hybrid is.
|
|
1555
1563
|
const self = this;
|
|
1556
|
-
|
|
1564
|
+
// With a preprocess pass the factory's function runs after it, as the
|
|
1565
|
+
// verdict does.
|
|
1566
|
+
const _gv = preprocess ? _rawVerdict : _fast === this._fastVerdict ? _fast : null;
|
|
1567
|
+
const _factory = (_vx === null && _gv !== null && typeof _gv._resultFactory === 'function') ? _gv._resultFactory : null;
|
|
1557
1568
|
if (_factory !== null) {
|
|
1558
1569
|
let calls = 0;
|
|
1559
1570
|
this.validate = (data) => {
|
|
1560
1571
|
if (++calls === HYBRID_TIER_CALLS && _onePass === null) {
|
|
1561
1572
|
const gen = _factory(LazyRejection, _errorsOf, _buildRawErrors, EMPTY_ERRORS);
|
|
1562
|
-
if (gen) self.validate = gen;
|
|
1573
|
+
if (gen) self.validate = preprocess ? (data) => { preprocess(data); return gen(data); } : gen;
|
|
1563
1574
|
}
|
|
1564
1575
|
return _plainValidate(data);
|
|
1565
1576
|
};
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.58.1",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -87,7 +87,8 @@
|
|
|
87
87
|
"browser": {
|
|
88
88
|
"./lib/native-load.js": "./lib/native-load.browser.js",
|
|
89
89
|
"./lib/aot.js": "./lib/aot.browser.js",
|
|
90
|
-
"./lib/buffer-gate.js": "./lib/buffer-gate.browser.js"
|
|
90
|
+
"./lib/buffer-gate.js": "./lib/buffer-gate.browser.js",
|
|
91
|
+
"./lib/interpreter.js": "./lib/interpreter.browser.js"
|
|
91
92
|
},
|
|
92
93
|
"scripts": {
|
|
93
94
|
"prepublishOnly": "node tests/test_pack_purity.js && ATA_ALLOW_DOC_PLACEHOLDERS=1 node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
|
|
@@ -185,13 +186,13 @@
|
|
|
185
186
|
"codegen.d.ts"
|
|
186
187
|
],
|
|
187
188
|
"optionalDependencies": {
|
|
188
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
189
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
190
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
191
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
192
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
193
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
194
|
-
"@ata-validator/native-win32-x64": "1.
|
|
189
|
+
"@ata-validator/native-darwin-arm64": "1.58.1",
|
|
190
|
+
"@ata-validator/native-darwin-x64": "1.58.1",
|
|
191
|
+
"@ata-validator/native-linux-arm64-gnu": "1.58.1",
|
|
192
|
+
"@ata-validator/native-linux-arm64-musl": "1.58.1",
|
|
193
|
+
"@ata-validator/native-linux-x64-gnu": "1.58.1",
|
|
194
|
+
"@ata-validator/native-linux-x64-musl": "1.58.1",
|
|
195
|
+
"@ata-validator/native-win32-x64": "1.58.1"
|
|
195
196
|
},
|
|
196
197
|
"peerDependencies": {
|
|
197
198
|
"yaml": "^2.0.0"
|