ata-validator 1.61.3 → 1.62.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 +67 -10
- package/bin/ata.js +3 -2
- package/compat.js +15 -2
- package/index.d.ts +7 -0
- package/index.js +10 -0
- package/lib/aot-build.js +27 -1
- package/lib/aot-impl.js +108 -17
- package/lib/buffer-gate.js +25 -14
- package/lib/bun-target.js +21 -5
- package/lib/clone-emit.js +115 -10
- package/lib/codegen-engine.js +94 -18
- package/lib/compiled-common.js +11 -4
- package/lib/compiled-verdict.js +2 -2
- package/lib/compiled.js +1 -1
- package/lib/data-positions.js +18 -0
- package/lib/defaults.js +101 -23
- package/lib/describe-schema.js +38 -5
- package/lib/diagnose.js +51 -29
- package/lib/enrich-error.js +23 -1
- package/lib/error-sort-rank.js +12 -0
- package/lib/formats-source.js +12 -1
- package/lib/formats.js +19 -19
- package/lib/interpreter-core.js +23 -4
- package/lib/js-compiler-errors.js +70 -72
- package/lib/js-compiler.js +212 -72
- package/lib/json-rejections.js +1 -1
- package/lib/migrate.js +24 -5
- package/lib/native-attach.browser.js +7 -0
- package/lib/native-attach.js +76 -0
- package/lib/native-positions.js +7 -0
- package/lib/plan-compiler.js +7 -1
- package/lib/regex-linear.js +88 -0
- package/lib/rejections.js +13 -3
- package/lib/render-compact.js +5 -5
- package/lib/render-pretty.js +23 -10
- package/lib/safe-regex.js +29 -4
- package/lib/scan-compiler.js +16 -4
- package/lib/stream.js +293 -0
- package/lib/tier0.js +6 -2
- package/lib/ts-gen.js +25 -12
- package/lib/unique-items.js +11 -3
- package/lib/validator-core.js +61 -66
- package/lib/verify.js +25 -6
- package/lib/version.js +1 -1
- package/package.json +17 -8
- package/stream.d.ts +51 -0
- package/stream.js +3 -0
- package/stream.mjs +3 -0
package/lib/clone-emit.js
CHANGED
|
@@ -50,6 +50,12 @@ 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
|
+
// A schema that embeds another resource (a subschema with its own `$id`)
|
|
54
|
+
// reads a fragment `$ref` inside it against that resource, not against
|
|
55
|
+
// this root, so the pointer resolved here could name a different schema
|
|
56
|
+
// from the one validation applies. No $ref is inlined there; emitClone
|
|
57
|
+
// then declines the $ref as it stands.
|
|
58
|
+
if (embedsResource(root, root, new Set())) return root;
|
|
53
59
|
// The runtime's useDefaults fills a property's default from the schema
|
|
54
60
|
// written under the property, the $ref node with any default next to it,
|
|
55
61
|
// and then the defaults inside the referenced definition, as it fills them
|
|
@@ -90,11 +96,83 @@ function inlineRefsForClone(root) {
|
|
|
90
96
|
return walk(root, new Set(), false);
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
function embedsResource(node, root, seen) {
|
|
100
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return false;
|
|
101
|
+
seen.add(node);
|
|
102
|
+
if (Array.isArray(node)) return node.some((n) => embedsResource(n, root, seen));
|
|
103
|
+
if (node !== root && typeof node.$id === 'string' && !node.$id.startsWith('#')) return true;
|
|
104
|
+
for (const k of Object.keys(node)) {
|
|
105
|
+
if (_INLINE_DATA_KEYS.has(k)) continue;
|
|
106
|
+
if (embedsResource(node[k], root, seen)) return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The names a node requires through itself and the in-place applicators
|
|
112
|
+
// below it (allOf, anyOf, oneOf, if, then, else, dependentRequired and the
|
|
113
|
+
// array form of dependencies). Every one has to be a declared property, or
|
|
114
|
+
// the copy, which keeps only declared keys, drops a key the schema demands
|
|
115
|
+
// and hands back a value validate() refuses. `not` is left out: a name
|
|
116
|
+
// required under it is one the value must not carry. Returns false where
|
|
117
|
+
// the walk cannot tell: dependentSchemas, the schema form of dependencies,
|
|
118
|
+
// or a $ref inside an applicator.
|
|
119
|
+
function requiredNames(node, out, depth) {
|
|
120
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return true;
|
|
121
|
+
if (depth > 16) return false;
|
|
122
|
+
if (depth > 0 && node.$ref !== undefined) return false;
|
|
123
|
+
if (node.dependentSchemas !== undefined) return false;
|
|
124
|
+
if (Array.isArray(node.required)) for (const n of node.required) out.add(n);
|
|
125
|
+
if (node.dependentRequired && typeof node.dependentRequired === 'object') {
|
|
126
|
+
for (const k of Object.keys(node.dependentRequired)) {
|
|
127
|
+
out.add(k);
|
|
128
|
+
const list = node.dependentRequired[k];
|
|
129
|
+
if (Array.isArray(list)) for (const n of list) out.add(n);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (node.dependencies !== undefined) {
|
|
133
|
+
if (!node.dependencies || typeof node.dependencies !== 'object') return false;
|
|
134
|
+
for (const k of Object.keys(node.dependencies)) {
|
|
135
|
+
const v = node.dependencies[k];
|
|
136
|
+
if (!Array.isArray(v)) return false;
|
|
137
|
+
out.add(k);
|
|
138
|
+
for (const n of v) out.add(n);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const k of ['allOf', 'anyOf', 'oneOf']) {
|
|
142
|
+
if (node[k] === undefined) continue;
|
|
143
|
+
if (!Array.isArray(node[k])) return false;
|
|
144
|
+
for (const b of node[k]) if (!requiredNames(b, out, depth + 1)) return false;
|
|
145
|
+
}
|
|
146
|
+
for (const k of ['if', 'then', 'else']) {
|
|
147
|
+
if (node[k] !== undefined && !requiredNames(node[k], out, depth + 1)) return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The source budget for default values written into one copy. A shared
|
|
153
|
+
// definition is inlined at every $ref to it, and its default with it: 200
|
|
154
|
+
// references to a 50 KB default made 10 MB of source. Past the budget the
|
|
155
|
+
// copy declines, as it does for anything else it cannot do well.
|
|
156
|
+
const DEFAULT_SOURCE_BUDGET = 256 * 1024;
|
|
157
|
+
|
|
158
|
+
// A default as source that builds it fresh on every parse. An object literal
|
|
159
|
+
// reads a "__proto__" key as the prototype slot, so a default holding one (an
|
|
160
|
+
// own key, as JSON.parse writes it) is rebuilt by JSON.parse from its text:
|
|
161
|
+
// as a literal, the filled value had no such key and inherited from the
|
|
162
|
+
// default's inner object. Null past the source budget.
|
|
163
|
+
function defaultSource(v, budget) {
|
|
164
|
+
const text = JSON.stringify(v);
|
|
165
|
+
const src = v !== null && typeof v === 'object' && text.includes('"__proto__"') ? `JSON.parse(${JSON.stringify(text)})` : text;
|
|
166
|
+
budget.left -= src.length;
|
|
167
|
+
return budget.left < 0 ? null : src;
|
|
168
|
+
}
|
|
169
|
+
|
|
93
170
|
const PROTO_KEY_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
|
|
94
171
|
|
|
95
|
-
function emitClone(node, access, depth) {
|
|
172
|
+
function emitClone(node, access, depth, budget) {
|
|
96
173
|
if (!node || typeof node !== 'object') return null;
|
|
97
174
|
if (depth > 12) return null;
|
|
175
|
+
if (budget === undefined) budget = { left: DEFAULT_SOURCE_BUDGET };
|
|
98
176
|
// Only plain object nodes are cloneable, and only when the schema names
|
|
99
177
|
// every property it keeps. A $ref means the set of allowed keys is not
|
|
100
178
|
// this node's to decide. In-place applicators (allOf, anyOf, oneOf,
|
|
@@ -151,6 +229,13 @@ function emitClone(node, access, depth) {
|
|
|
151
229
|
for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
|
|
152
230
|
}
|
|
153
231
|
const required = new Set(Array.isArray(node.required) ? node.required : []);
|
|
232
|
+
// Outside the record shape, where every key is kept, every name the
|
|
233
|
+
// schema requires must be one the copy keeps.
|
|
234
|
+
if (!apSchema) {
|
|
235
|
+
const demanded = new Set();
|
|
236
|
+
if (!requiredNames(node, demanded, 0)) return null;
|
|
237
|
+
for (const n of demanded) if (!node.properties || !Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
|
|
238
|
+
}
|
|
154
239
|
|
|
155
240
|
const fixed = [];
|
|
156
241
|
const conditional = [];
|
|
@@ -174,7 +259,7 @@ function emitClone(node, access, depth) {
|
|
|
174
259
|
if (!probe || !probe(prop.default)) return null;
|
|
175
260
|
}
|
|
176
261
|
const read = `${access}[${JSON.stringify(key)}]`;
|
|
177
|
-
const value = emitValueExpr(prop, read, depth);
|
|
262
|
+
const value = emitValueExpr(prop, read, depth, budget);
|
|
178
263
|
if (value === null) return null;
|
|
179
264
|
// A plain `"__proto__":` in an object literal is the prototype slot, not
|
|
180
265
|
// a property; `["__proto__"]:` is a property. Only that key is written as
|
|
@@ -197,7 +282,7 @@ function emitClone(node, access, depth) {
|
|
|
197
282
|
if (apSchema) {
|
|
198
283
|
const kVar = `_k${depth}`;
|
|
199
284
|
const vVar = `_v${depth}`;
|
|
200
|
-
const valueExpr = emitValueExpr(apSchema, vVar, depth);
|
|
285
|
+
const valueExpr = emitValueExpr(apSchema, vVar, depth, budget);
|
|
201
286
|
if (valueExpr === null) return null;
|
|
202
287
|
let skip = '';
|
|
203
288
|
let declSet = '';
|
|
@@ -217,8 +302,15 @@ function emitClone(node, access, depth) {
|
|
|
217
302
|
if (conditional.length === 0 && !recordLoop) return literal;
|
|
218
303
|
// Optional properties are added only when present, so the result never
|
|
219
304
|
// gains a key the input did not have.
|
|
305
|
+
const dfltSrc = [];
|
|
306
|
+
for (const c of conditional) {
|
|
307
|
+
if (c.dflt === undefined) { dfltSrc.push(null); continue; }
|
|
308
|
+
const src = defaultSource(c.dflt, budget);
|
|
309
|
+
if (src === null) return null;
|
|
310
|
+
dfltSrc.push(src);
|
|
311
|
+
}
|
|
220
312
|
const adds = conditional
|
|
221
|
-
.map(({ key, value, dflt }) => {
|
|
313
|
+
.map(({ key, value, dflt }, i) => {
|
|
222
314
|
// Object.hasOwn, not `in`: `in` also answers for what the instance
|
|
223
315
|
// inherits, so a property named `constructor` would copy the inherited
|
|
224
316
|
// function into the output that validate().data does not carry. And a
|
|
@@ -235,12 +327,12 @@ function emitClone(node, access, depth) {
|
|
|
235
327
|
? `Object.prototype.hasOwnProperty.call(${access}, ${k})`
|
|
236
328
|
: `(${k} in ${access} && (${access}.__proto__ === Object.prototype || Object.prototype.hasOwnProperty.call(${access}, ${k})))`;
|
|
237
329
|
const set = `if (${own}) ${assign}`;
|
|
238
|
-
// The default is emitted as
|
|
330
|
+
// The default is emitted as source inside the call, so an object or
|
|
239
331
|
// array default is a fresh value on every parse, never shared state.
|
|
240
332
|
if (dflt === undefined) return set;
|
|
241
333
|
const setDflt = key === '__proto__'
|
|
242
|
-
? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${
|
|
243
|
-
: `${tmp}[${JSON.stringify(key)}] = ${
|
|
334
|
+
? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${dfltSrc[i]}, writable: true, enumerable: true, configurable: true });`
|
|
335
|
+
: `${tmp}[${JSON.stringify(key)}] = ${dfltSrc[i]};`;
|
|
244
336
|
return `${set} else ${setDflt}`;
|
|
245
337
|
})
|
|
246
338
|
.join(' ');
|
|
@@ -278,9 +370,22 @@ function isPrimitiveOnly(s) {
|
|
|
278
370
|
return false;
|
|
279
371
|
}
|
|
280
372
|
|
|
281
|
-
function emitValueExpr(prop, read, depth) {
|
|
373
|
+
function emitValueExpr(prop, read, depth, budget) {
|
|
282
374
|
if (!prop || typeof prop !== 'object') return null;
|
|
283
375
|
if (prop.$ref) return null;
|
|
376
|
+
// Positions named by prefixItems (or the array form of items, which
|
|
377
|
+
// normalization turns into prefixItems) each have their own schema, and
|
|
378
|
+
// copying them all with the strategy for `items` handed back an object in
|
|
379
|
+
// a prefix slot as the input's own reference, its undeclared keys kept.
|
|
380
|
+
// A tuple of primitives, with nothing or primitives after it, has nowhere
|
|
381
|
+
// to hide a key, and is copied as a fresh array like any other.
|
|
382
|
+
if (prop.prefixItems !== undefined || Array.isArray(prop.items)) {
|
|
383
|
+
const tuple = Array.isArray(prop.prefixItems) ? prop.prefixItems : prop.items;
|
|
384
|
+
const rest = Array.isArray(prop.items) ? prop.additionalItems : prop.items;
|
|
385
|
+
if (prop.type === 'array' && Array.isArray(tuple) && tuple.every(isPrimitiveOnly) &&
|
|
386
|
+
(rest === false || (rest !== undefined && isPrimitiveOnly(rest)))) return `${read}.slice()`;
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
284
389
|
if (isPrimitiveOnly(prop)) return read;
|
|
285
390
|
// A value checked with `instanceof` (the keyword @ata-project/keywords adds)
|
|
286
391
|
// is an instance of a class, such as a Date, not a JSON object whose keys
|
|
@@ -290,7 +395,7 @@ function emitValueExpr(prop, read, depth) {
|
|
|
290
395
|
const isRecordable = (s) => s.properties ||
|
|
291
396
|
(s.additionalProperties && typeof s.additionalProperties === 'object');
|
|
292
397
|
if (prop.type === 'object' && isRecordable(prop)) {
|
|
293
|
-
return emitClone(prop, read, depth + 1);
|
|
398
|
+
return emitClone(prop, read, depth + 1, budget);
|
|
294
399
|
}
|
|
295
400
|
if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
|
|
296
401
|
!Array.isArray(prop.items)) {
|
|
@@ -300,7 +405,7 @@ function emitValueExpr(prop, read, depth) {
|
|
|
300
405
|
if (isPrimitiveOnly(it)) return `${read}.slice()`;
|
|
301
406
|
if (it.type === 'object' && isRecordable(it)) {
|
|
302
407
|
const el = '_e' + depth;
|
|
303
|
-
const inner = emitClone(it, el, depth + 1);
|
|
408
|
+
const inner = emitClone(it, el, depth + 1, budget);
|
|
304
409
|
if (!inner) return null;
|
|
305
410
|
return `${read}.map((${el}) => (${inner}))`;
|
|
306
411
|
}
|
package/lib/codegen-engine.js
CHANGED
|
@@ -440,7 +440,7 @@ function installCodegenPaths (ctx) {
|
|
|
440
440
|
let nativeText; // undefined until the first large document
|
|
441
441
|
const nativeVerdict = (jsonStr) => {
|
|
442
442
|
if (nativeText === undefined) {
|
|
443
|
-
nativeText = !!getNative() && !require('./buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords);
|
|
443
|
+
nativeText = !!getNative() && !require('./buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords, this._userFormats);
|
|
444
444
|
if (nativeText) {
|
|
445
445
|
this._ensureNative();
|
|
446
446
|
if (!(this._fastSlot >= 0)) nativeText = false;
|
|
@@ -715,7 +715,7 @@ installCodegenPaths.installScanner = function installScanner (schemaObj, options
|
|
|
715
715
|
// access expression read through a parent that could be absent or null:
|
|
716
716
|
// `{ c: { properties: { d: { additionalProperties: false } } } }` threw a
|
|
717
717
|
// TypeError from validate({}) whenever the optional `c` was missing.
|
|
718
|
-
function emitRemovals(node, access, lines, depth, seen) {
|
|
718
|
+
function emitRemovals(node, access, lines, depth, seen, own) {
|
|
719
719
|
if (!node || typeof node !== 'object' || !node.properties) return;
|
|
720
720
|
if (seen.has(node)) return;
|
|
721
721
|
seen.add(node);
|
|
@@ -733,13 +733,15 @@ function emitRemovals(node, access, lines, depth, seen) {
|
|
|
733
733
|
for (const key of keys) {
|
|
734
734
|
const prop = node.properties[key];
|
|
735
735
|
if (prop && typeof prop === 'object' && prop.properties) {
|
|
736
|
-
|
|
736
|
+
// Into own keys only: a nested object the document inherits is not
|
|
737
|
+
// part of it, and deleting from it would strip a shared object.
|
|
738
|
+
emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, body, depth + 1, seen, `Object.hasOwn(${access},${JSON.stringify(key)})&&`);
|
|
737
739
|
}
|
|
738
740
|
}
|
|
739
741
|
seen.delete(node);
|
|
740
742
|
if (body.length === 0) return;
|
|
741
743
|
if (depth === 0) lines.push(...body);
|
|
742
|
-
else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
|
|
744
|
+
else lines.push(`if(${own}${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
|
|
743
745
|
}
|
|
744
746
|
|
|
745
747
|
// The coercions the closure pass applies, emitted. Values: `number` and
|
|
@@ -779,6 +781,15 @@ function emitCoercions(node, ov, lines, st, depth) {
|
|
|
779
781
|
if (st.seen.has(node)) { st.cycle = true; return; }
|
|
780
782
|
st.seen.add(node);
|
|
781
783
|
if (node.properties) {
|
|
784
|
+
// Own properties only, as the closure pass has it: a value the object
|
|
785
|
+
// inherits belongs to some other object, and `k in o` once wrapped
|
|
786
|
+
// Object.prototype's own `constructor` into an own array key. On a plain
|
|
787
|
+
// object (prototype Object.prototype) a read that gives a primitive or
|
|
788
|
+
// an object is own for every key but the ones Object.prototype has, so
|
|
789
|
+
// the flag settles the common case and hasOwn is consulted otherwise.
|
|
790
|
+
const pk = '_pk' + st.n++;
|
|
791
|
+
let flagged = false;
|
|
792
|
+
const flag = () => { if (!flagged) { flagged = true; lines.push(`const ${pk}=Object.getPrototypeOf(${ov})===Object.prototype`); } };
|
|
782
793
|
for (const [key, prop] of Object.entries(node.properties)) {
|
|
783
794
|
// Coercion writes with plain assignment, which for a key named
|
|
784
795
|
// __proto__ rewrites the prototype instead. The raw value still goes
|
|
@@ -786,13 +797,15 @@ function emitCoercions(node, ov, lines, st, depth) {
|
|
|
786
797
|
if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
|
|
787
798
|
const k = JSON.stringify(key);
|
|
788
799
|
const a = `${ov}[${k}]`;
|
|
789
|
-
|
|
800
|
+
const own = key in Object.prototype ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
|
|
801
|
+
if (isScalarCoercible(prop)) { flag(); lines.push(`if(${own}){`, ...scalarCoercion(a, prop.type), '}'); }
|
|
790
802
|
// Wrapping a lone value in an array is a top-level rule only, as it was.
|
|
791
|
-
else if (depth === 0 && prop.type === 'array' && st.arrayMode) lines.push(`if(${
|
|
803
|
+
else if (depth === 0 && prop.type === 'array' && st.arrayMode) { flag(); lines.push(`if(${own}&&${a}!==undefined&&!Array.isArray(${a}))${a}=[${a}]`); }
|
|
792
804
|
// A leaf, the common case, has nothing below it to coerce.
|
|
793
805
|
if ((prop.properties || prop.items) && coercesInside(prop, new Set(), st.memo)) {
|
|
806
|
+
flag();
|
|
794
807
|
const n = '_c' + st.n++;
|
|
795
|
-
lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null){`);
|
|
808
|
+
lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
|
|
796
809
|
emitCoercions(prop, n, lines, st, depth + 1);
|
|
797
810
|
lines.push('}}');
|
|
798
811
|
}
|
|
@@ -815,14 +828,23 @@ function emitCoercions(node, ov, lines, st, depth) {
|
|
|
815
828
|
}
|
|
816
829
|
// From the closure pass, on the first reference a defaults walk meets: loaded
|
|
817
830
|
// with this module it joined every process's cold start.
|
|
818
|
-
|
|
819
|
-
|
|
831
|
+
// A fragment `$ref` is read against the resource its node belongs to (an
|
|
832
|
+
// embedded `$id`), as validation and the closure pass read it.
|
|
833
|
+
let _defaultsMod = null;
|
|
834
|
+
const defaultsMod = () => _defaultsMod || (_defaultsMod = require('./defaults'));
|
|
835
|
+
const refOf = (rs, node) => {
|
|
836
|
+
if (typeof node.$ref !== 'string') return null;
|
|
837
|
+
if (rs.res === undefined) rs.res = defaultsMod().resourceRoots(rs.root);
|
|
838
|
+
return defaultsMod().refTarget(rs.root, rs.res, node);
|
|
839
|
+
};
|
|
820
840
|
// Whether a default is reachable from a schema through the places the pass
|
|
821
841
|
// goes (lib/defaults.js reachesDefault, which this pass is held to): the
|
|
822
842
|
// same walk, with answers kept per schema object for the whole pass, except
|
|
823
843
|
// one that rests on a schema still being decided (a recursive reference). It
|
|
824
844
|
// started over for every property, quadratic in the depth of the schema.
|
|
825
|
-
|
|
845
|
+
// `res`, the resources map, is built on the first `$ref` the walk follows:
|
|
846
|
+
// most schemas have none, and the walk over the schema is not worth making.
|
|
847
|
+
function reachState(root) { return { root, memo: new Map(), res: undefined }; }
|
|
826
848
|
function hasDefaultsInside(node, rs) { return reachDefault(node, rs, new Set()) === 1; }
|
|
827
849
|
function reachDefault(node, rs, stack) {
|
|
828
850
|
if (!node || typeof node !== 'object') return 0;
|
|
@@ -838,7 +860,7 @@ function reachDefault(node, rs, stack) {
|
|
|
838
860
|
if (prop && typeof prop === 'object' && (prop.default !== undefined || check(prop))) { yes = true; break; }
|
|
839
861
|
}
|
|
840
862
|
}
|
|
841
|
-
if (!yes && typeof node.$ref === 'string') yes = check(
|
|
863
|
+
if (!yes && typeof node.$ref === 'string') yes = check(refOf(rs, node));
|
|
842
864
|
if (!yes) for (const list of [node.allOf, node.prefixItems]) { if (Array.isArray(list) && list.some(check)) { yes = true; break; } }
|
|
843
865
|
if (!yes) for (const one of [node.items, node.additionalProperties]) { if (check(one)) { yes = true; break; } }
|
|
844
866
|
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; } }
|
|
@@ -868,6 +890,8 @@ function emitDefaultsAt(schema, expr, lines, st) {
|
|
|
868
890
|
// A pass that would expand to more than this many lines (references written
|
|
869
891
|
// out again at every use) is left to the closure pass.
|
|
870
892
|
const DEFAULTS_MAX_LINES = 4000;
|
|
893
|
+
// The most default-value source the generated pass writes, in characters.
|
|
894
|
+
const DEFAULTS_MAX_BYTES = 256 * 1024;
|
|
871
895
|
// How many schema objects emitDefaults would write out, references expanded
|
|
872
896
|
// at every use, or Infinity where it would decline (a cycle, or defaults
|
|
873
897
|
// under patternProperties). Each writes at least one line, so a count over
|
|
@@ -886,13 +910,22 @@ function defaultsVisits(node, st, stack) {
|
|
|
886
910
|
add(node.items);
|
|
887
911
|
add(node.additionalProperties);
|
|
888
912
|
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') Object.values(node.dependentSchemas).forEach(add);
|
|
889
|
-
if (typeof node.$ref === 'string') add(
|
|
913
|
+
if (typeof node.$ref === 'string') add(refOf(st.reach, node));
|
|
890
914
|
if (Array.isArray(node.allOf)) node.allOf.forEach(add);
|
|
891
915
|
stack.delete(node);
|
|
892
916
|
if (n > DEFAULTS_MAX_LINES) n = Infinity;
|
|
893
917
|
st.visits.set(node, n);
|
|
894
918
|
return n;
|
|
895
919
|
}
|
|
920
|
+
// A default as source that rebuilds it fresh on every fill. An object
|
|
921
|
+
// literal reads a "__proto__" key as the prototype slot, so a default that
|
|
922
|
+
// holds one (own, as JSON.parse writes it) is rebuilt by JSON.parse from its
|
|
923
|
+
// text instead: as a literal the filled value had no such key and inherited
|
|
924
|
+
// from the default's inner object.
|
|
925
|
+
function defaultLiteral(v) {
|
|
926
|
+
const text = JSON.stringify(v);
|
|
927
|
+
return v !== null && typeof v === 'object' && text.includes('"__proto__"') ? `JSON.parse(${JSON.stringify(text)})` : text;
|
|
928
|
+
}
|
|
896
929
|
function emitDefaults(node, ov, lines, st) {
|
|
897
930
|
if (st.seen.has(node) || lines.length > DEFAULTS_MAX_LINES) { st.cycle = true; return; }
|
|
898
931
|
st.seen.add(node);
|
|
@@ -915,7 +948,13 @@ function emitDefaults(node, ov, lines, st) {
|
|
|
915
948
|
const inherited = key in Object.prototype;
|
|
916
949
|
const absent = inherited ? `!Object.hasOwn(${ov},${k})` : `(${ov}[${k}]===undefined||!${pk})&&!Object.hasOwn(${ov},${k})`;
|
|
917
950
|
if (prop.default !== undefined) {
|
|
918
|
-
const def =
|
|
951
|
+
const def = defaultLiteral(prop.default);
|
|
952
|
+
// A shared definition is written out at every reference to it, its
|
|
953
|
+
// defaults with it: 200 references to a 50 KB default made 10 MB of
|
|
954
|
+
// source. Past the budget the closure pass, which holds each default
|
|
955
|
+
// once, takes the schema.
|
|
956
|
+
st.defaultBytes = (st.defaultBytes || 0) + def.length;
|
|
957
|
+
if (st.defaultBytes > DEFAULTS_MAX_BYTES) { st.cycle = true; st.seen.delete(node); return; }
|
|
919
958
|
// Assignment to a key named __proto__ hits the prototype setter
|
|
920
959
|
// instead of creating a property; defineProperty writes an own key.
|
|
921
960
|
lines.push(key === '__proto__'
|
|
@@ -978,7 +1017,7 @@ function emitDefaults(node, ov, lines, st) {
|
|
|
978
1017
|
// written out here (one back into what is being written is a cycle, left
|
|
979
1018
|
// to the closure pass), then each `allOf` member, in the closure pass's order.
|
|
980
1019
|
if (typeof node.$ref === 'string') {
|
|
981
|
-
const target =
|
|
1020
|
+
const target = refOf(st.reach, node);
|
|
982
1021
|
if (target && hasDefaultsInside(target, st.reach)) emitDefaults(target, ov, lines, st);
|
|
983
1022
|
}
|
|
984
1023
|
if (Array.isArray(node.allOf)) {
|
|
@@ -1075,8 +1114,15 @@ function buildParse (self, decline, extended) {
|
|
|
1075
1114
|
self._ensureCompiled();
|
|
1076
1115
|
const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
|
|
1077
1116
|
if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
|
|
1078
|
-
|
|
1079
|
-
|
|
1117
|
+
// With a preprocess pass (defaults, removeAdditional), validate() checks
|
|
1118
|
+
// the document after the pass has run, and a check of the raw input can
|
|
1119
|
+
// answer the other way: a default can make a conditional apply. The
|
|
1120
|
+
// verdict is then taken on a filled clone of the input, the caller's
|
|
1121
|
+
// object left alone, and the copy handed back is checked as well, so
|
|
1122
|
+
// parse() never returns a value validate() would refuse.
|
|
1123
|
+
const fill = self._preprocess;
|
|
1124
|
+
const filled = typeof fill === 'function';
|
|
1125
|
+
const reject = (data) => {
|
|
1080
1126
|
const e = new Error('validation failed');
|
|
1081
1127
|
e.name = 'AtaValidationError';
|
|
1082
1128
|
let target = data;
|
|
@@ -1099,8 +1145,28 @@ function buildParse (self, decline, extended) {
|
|
|
1099
1145
|
set (v) { Object.defineProperty(e, 'errors', { value: v, writable: true, configurable: true, enumerable: true }); },
|
|
1100
1146
|
});
|
|
1101
1147
|
throw e;
|
|
1148
|
+
};
|
|
1149
|
+
if (!filled) {
|
|
1150
|
+
return (data) => {
|
|
1151
|
+
if (!verdict(data)) reject(data);
|
|
1152
|
+
return copy(data);
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
return (data) => {
|
|
1156
|
+
let doc;
|
|
1157
|
+
try { doc = structuredClone(data); } catch { doc = undefined; }
|
|
1158
|
+
if (doc === undefined && data !== undefined) {
|
|
1159
|
+
throw new TypeError('parse() is not available for this value: it cannot be copied to fill its defaults before the check. Use validate() instead.');
|
|
1160
|
+
}
|
|
1161
|
+
if (doc !== null && typeof doc === 'object') fill(doc);
|
|
1162
|
+
if (!verdict(doc)) reject(data);
|
|
1163
|
+
// The copy is built from the input, filling its defaults as written, as
|
|
1164
|
+
// it is without a preprocess pass.
|
|
1165
|
+
const out = copy(data);
|
|
1166
|
+
if (!verdict(out)) {
|
|
1167
|
+
throw new TypeError('parse() is not available for this value: the copy that keeps the declared properties is not one the schema accepts. Use validate() instead.');
|
|
1102
1168
|
}
|
|
1103
|
-
return
|
|
1169
|
+
return out;
|
|
1104
1170
|
};
|
|
1105
1171
|
}
|
|
1106
1172
|
|
|
@@ -1147,7 +1213,17 @@ function coldTwin (self, Validator) {
|
|
|
1147
1213
|
if (t === undefined) {
|
|
1148
1214
|
t = null;
|
|
1149
1215
|
if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
|
|
1150
|
-
try {
|
|
1216
|
+
try {
|
|
1217
|
+
t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' }));
|
|
1218
|
+
// The registry as it is now, not as the constructor options had it:
|
|
1219
|
+
// a schema registered with addSchema() after construction is in the
|
|
1220
|
+
// live map only, and a twin without it answered against the schema
|
|
1221
|
+
// the registry had replaced.
|
|
1222
|
+
if (self._schemaMap && self._schemaMap.size > 0) {
|
|
1223
|
+
t._schemaMap = new Map(self._schemaMap);
|
|
1224
|
+
t._schemaMapShared = false;
|
|
1225
|
+
}
|
|
1226
|
+
} catch { t = null; }
|
|
1151
1227
|
}
|
|
1152
1228
|
self._twin = t;
|
|
1153
1229
|
}
|
package/lib/compiled-common.js
CHANGED
|
@@ -109,13 +109,20 @@ function combine(a, b) {
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// The JSON entry points under a check, as the runtime has them.
|
|
113
|
-
|
|
112
|
+
// The JSON entry points under a check, as the runtime has them. `fill` is
|
|
113
|
+
// the wrapper's defaults pass, or null: the check sees the document the
|
|
114
|
+
// schema checked, defaults filled, not the raw text.
|
|
115
|
+
function extendJSON(base, { check, errors }, fill) {
|
|
116
|
+
const prepared = (text) => {
|
|
117
|
+
const p = parse(text);
|
|
118
|
+
if (!p.error && fill) fill(p.value);
|
|
119
|
+
return p;
|
|
120
|
+
};
|
|
114
121
|
return {
|
|
115
122
|
validateJSON: base.validateJSON && ((text) => {
|
|
116
123
|
const res = base.validateJSON(text);
|
|
117
124
|
if (!res.valid) return res;
|
|
118
|
-
const p =
|
|
125
|
+
const p = prepared(text);
|
|
119
126
|
if (p.error) return res;
|
|
120
127
|
if (check(p.value)) return res;
|
|
121
128
|
const e = errors(p.value);
|
|
@@ -123,7 +130,7 @@ function extendJSON(base, { check, errors }) {
|
|
|
123
130
|
}),
|
|
124
131
|
isValidJSON: (text) => {
|
|
125
132
|
if (!base.isValidJSON(text)) return false;
|
|
126
|
-
const p =
|
|
133
|
+
const p = prepared(text);
|
|
127
134
|
return p.error ? true : check(p.value);
|
|
128
135
|
},
|
|
129
136
|
};
|
package/lib/compiled-verdict.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const { prepare, parse, installExtension, extendJSON } = require('./compiled-common');
|
|
11
11
|
|
|
12
12
|
function fromCompiledVerdict(mod, schema, options) {
|
|
13
|
-
const { isValid } = prepare(mod, schema, options, 'fromCompiledVerdict');
|
|
13
|
+
const { fill, isValid } = prepare(mod, schema, options, 'fromCompiledVerdict');
|
|
14
14
|
const api = {
|
|
15
15
|
isValidObject(data) {
|
|
16
16
|
return isValid(data);
|
|
@@ -22,7 +22,7 @@ function fromCompiledVerdict(mod, schema, options) {
|
|
|
22
22
|
};
|
|
23
23
|
installExtension(api, schema, (base, ext) => ({
|
|
24
24
|
isValidObject: (d) => base.isValidObject(d) && ext.check(d),
|
|
25
|
-
isValidJSON: extendJSON(base, ext).isValidJSON,
|
|
25
|
+
isValidJSON: extendJSON(base, ext, fill).isValidJSON,
|
|
26
26
|
}));
|
|
27
27
|
return api;
|
|
28
28
|
}
|
package/lib/compiled.js
CHANGED
|
@@ -87,7 +87,7 @@ function fromCompiled(mod, schema, options) {
|
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
installExtension(api, schema, (base, ext) => {
|
|
90
|
-
const json = extendJSON(base, ext);
|
|
90
|
+
const json = extendJSON(base, ext, fill);
|
|
91
91
|
return {
|
|
92
92
|
// The verdict is the schema's and the check's. A failure lists the
|
|
93
93
|
// schema's errors, then the check's, read when somebody asks.
|
package/lib/data-positions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
/**
|
|
4
5
|
* Build pointer → { byteOffset, length, line, col, text } from a JSON
|
|
5
6
|
* input buffer. Called only when validation fails AND richErrors is on
|
|
@@ -156,6 +157,16 @@ function buildDataPositionMap (input) {
|
|
|
156
157
|
col: off - lineStart[line] + 1,
|
|
157
158
|
text: lines[line] || '',
|
|
158
159
|
};
|
|
160
|
+
// A minified document is one line, and each error carried all of it:
|
|
161
|
+
// 4000 errors on a 100 KB line serialized to 800 MB. A long line is cut
|
|
162
|
+
// to 200 characters from 60 before the value, and textCol says where the
|
|
163
|
+
// value starts in the cut text; col stays the column in the line.
|
|
164
|
+
if (entry.text.length > 200) {
|
|
165
|
+
let s = entry.col > 61 ? entry.col - 61 : 0;
|
|
166
|
+
if (s + 200 > entry.text.length) s = entry.text.length - 200;
|
|
167
|
+
entry.text = entry.text.slice(s, s + 200);
|
|
168
|
+
entry.textCol = entry.col - s;
|
|
169
|
+
}
|
|
159
170
|
if (keyStarts[k] >= 0) {
|
|
160
171
|
const keyLine = lineOf(keyStarts[k]);
|
|
161
172
|
entry.keyOffset = keyStarts[k];
|
|
@@ -355,6 +366,13 @@ function buildTargetedPositionMap (input, wanted) {
|
|
|
355
366
|
col: off - lineStart[line] + 1,
|
|
356
367
|
text: lines[line] || '',
|
|
357
368
|
};
|
|
369
|
+
// A long line is cut around the value, as in buildDataPositionMap.
|
|
370
|
+
if (entry.text.length > 200) {
|
|
371
|
+
let s = entry.col > 61 ? entry.col - 61 : 0;
|
|
372
|
+
if (s + 200 > entry.text.length) s = entry.text.length - 200;
|
|
373
|
+
entry.text = entry.text.slice(s, s + 200);
|
|
374
|
+
entry.textCol = entry.col - s;
|
|
375
|
+
}
|
|
358
376
|
if (keyStarts[k] >= 0) {
|
|
359
377
|
const keyLine = lineOf(keyStarts[k]);
|
|
360
378
|
entry.keyOffset = keyStarts[k];
|