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/stream.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Validation as the body arrives. A request that carries many documents, a
|
|
4
|
+
// JSON array or NDJSON, is today read to the end, parsed as a whole and then
|
|
5
|
+
// validated, so a bad third record is reported after the last byte: on a
|
|
6
|
+
// 100 MB upload that is half a second and 500 to 800 MB of memory before the
|
|
7
|
+
// caller hears "record 2, email". Here each record is validated the moment
|
|
8
|
+
// its last byte lands, so the same upload is refused after 100 KB, in
|
|
9
|
+
// milliseconds, with memory that does not grow with the body. Measured in
|
|
10
|
+
// the commit that added this: 517 ms and 783 MB to 6 ms and 114 MB for an
|
|
11
|
+
// early failure; the late failure costs the same time with a third of the
|
|
12
|
+
// memory.
|
|
13
|
+
//
|
|
14
|
+
// The splitting reads the text once and parses nothing it does not have to.
|
|
15
|
+
// NDJSON is split at newlines. A JSON array is split by a scan that tracks
|
|
16
|
+
// strings and nesting depth and hands out each top-level element; inside a
|
|
17
|
+
// string it jumps to the next quote with indexOf and counts the backslashes
|
|
18
|
+
// before it, so string bodies, most of a document's bytes, cost no branch
|
|
19
|
+
// per character. Each element is then parsed and validated on its own, by
|
|
20
|
+
// the validator's own verdict and, when it fails, its own error list.
|
|
21
|
+
|
|
22
|
+
const PARSE_KEYWORD = '__parse__';
|
|
23
|
+
|
|
24
|
+
// A record's failure as the stream reports it: the record's index from zero
|
|
25
|
+
// and the errors validate() would give for that document, or the parse
|
|
26
|
+
// error in the shape validateJSON() uses for a document that is not JSON.
|
|
27
|
+
function failure (record, errors) {
|
|
28
|
+
return { valid: false, record, errors };
|
|
29
|
+
}
|
|
30
|
+
function parseFailure (record, message) {
|
|
31
|
+
return failure(record, [{ keyword: PARSE_KEYWORD, instancePath: '', schemaPath: '', params: {}, message: 'invalid JSON: ' + message }]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The decoder for byte chunks: a multi-byte character split across two
|
|
35
|
+
// chunks is completed by the next one. Strings pass through untouched.
|
|
36
|
+
function decoderFor () {
|
|
37
|
+
const d = typeof TextDecoder === 'function' ? new TextDecoder('utf-8') : null;
|
|
38
|
+
return (chunk, last) => {
|
|
39
|
+
if (typeof chunk === 'string') return chunk;
|
|
40
|
+
if (d === null) return Buffer.from(chunk).toString('utf8');
|
|
41
|
+
return d.decode(chunk, { stream: !last });
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The end of the string that opens at `from` (the index after the quote), or
|
|
46
|
+
// -1 when the chunk ends first. A quote preceded by an odd number of
|
|
47
|
+
// backslashes is escaped.
|
|
48
|
+
function stringEnd (s, from) {
|
|
49
|
+
let i = from;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const q = s.indexOf('"', i);
|
|
52
|
+
if (q < 0) return -1;
|
|
53
|
+
let bs = 0;
|
|
54
|
+
for (let k = q - 1; k >= from && s.charCodeAt(k) === 92; k--) bs++;
|
|
55
|
+
if ((bs & 1) === 0) return q;
|
|
56
|
+
i = q + 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isSpace (c) { return c === 32 || c === 10 || c === 13 || c === 9; }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A record stream over one validator. `push(chunk)` takes a string, Buffer or
|
|
64
|
+
* Uint8Array and returns null while every record so far is valid, or the
|
|
65
|
+
* first failure as soon as a record fails (and the stream is then done).
|
|
66
|
+
* `end()` closes the input and returns `{ valid, records, errors }`, where
|
|
67
|
+
* `errors` lists every failure seen (one, unless `stopOnFirst` is false).
|
|
68
|
+
*/
|
|
69
|
+
class RecordStream {
|
|
70
|
+
constructor (validator, opts) {
|
|
71
|
+
this._v = validator;
|
|
72
|
+
this._format = (opts && opts.format) || 'auto';
|
|
73
|
+
this._stop = !(opts && opts.stopOnFirst === false);
|
|
74
|
+
this._onRecord = (opts && typeof opts.onRecord === 'function') ? opts.onRecord : null;
|
|
75
|
+
this._decode = decoderFor();
|
|
76
|
+
this._buf = '';
|
|
77
|
+
this._pos = 0; // scan position in _buf
|
|
78
|
+
this._start = -1; // where the element in progress begins, array form
|
|
79
|
+
this._depth = 0; // nesting inside the current element, array form
|
|
80
|
+
this._opened = false; // the outer '[' has been read
|
|
81
|
+
this._closed = false; // the outer ']' has been read
|
|
82
|
+
this._expectValue = true; // array form: a value may start here (after '[' or ',')
|
|
83
|
+
this.records = 0;
|
|
84
|
+
this.failures = [];
|
|
85
|
+
this.done = false;
|
|
86
|
+
this.result = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// One record's text: parse, validate, report. Returns the failure or null.
|
|
90
|
+
_record (text) {
|
|
91
|
+
const index = this.records++;
|
|
92
|
+
let doc;
|
|
93
|
+
try { doc = JSON.parse(text); } catch (e) { return this._fail(parseFailure(index, e.message)); }
|
|
94
|
+
if (this._v.isValidObject(doc)) {
|
|
95
|
+
if (this._onRecord !== null) this._onRecord(doc, index);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const r = this._v.validate(doc);
|
|
99
|
+
return this._fail(failure(index, r.valid ? [] : r.errors));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_fail (f) {
|
|
103
|
+
this.failures.push(f);
|
|
104
|
+
if (this._stop) { this.done = true; this.result = { valid: false, records: this.records, errors: this.failures }; }
|
|
105
|
+
return f;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
push (chunk) {
|
|
109
|
+
if (this.done) return this.failures.length ? this.failures[0] : null;
|
|
110
|
+
const text = this._decode(chunk, false);
|
|
111
|
+
if (text.length === 0) return null;
|
|
112
|
+
this._buf += text;
|
|
113
|
+
if (this._format === 'auto') {
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < this._buf.length && isSpace(this._buf.charCodeAt(i))) i++;
|
|
116
|
+
if (i === this._buf.length) return null;
|
|
117
|
+
this._format = this._buf.charCodeAt(i) === 91 ? 'array' : 'ndjson';
|
|
118
|
+
}
|
|
119
|
+
return this._format === 'array' ? this._scanArray() : this._scanLines();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_scanLines () {
|
|
123
|
+
const s = this._buf;
|
|
124
|
+
let from = 0;
|
|
125
|
+
for (;;) {
|
|
126
|
+
const nl = s.indexOf('\n', from);
|
|
127
|
+
if (nl < 0) break;
|
|
128
|
+
let end = nl;
|
|
129
|
+
if (end > from && s.charCodeAt(end - 1) === 13) end--;
|
|
130
|
+
const line = s.slice(from, end);
|
|
131
|
+
from = nl + 1;
|
|
132
|
+
if (line.trim().length === 0) continue;
|
|
133
|
+
const f = this._record(line);
|
|
134
|
+
if (f !== null && this.done) { this._buf = ''; return f; }
|
|
135
|
+
}
|
|
136
|
+
this._buf = from === 0 ? s : s.slice(from);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_scanArray () {
|
|
141
|
+
const s = this._buf;
|
|
142
|
+
const n = s.length;
|
|
143
|
+
let pos = this._pos;
|
|
144
|
+
while (pos < n) {
|
|
145
|
+
const c = s.charCodeAt(pos);
|
|
146
|
+
if (!this._opened) {
|
|
147
|
+
if (isSpace(c)) { pos++; continue; }
|
|
148
|
+
if (c !== 91) return this._syntax('a JSON array must start with "["');
|
|
149
|
+
this._opened = true; pos++; continue;
|
|
150
|
+
}
|
|
151
|
+
if (this._closed) {
|
|
152
|
+
if (isSpace(c)) { pos++; continue; }
|
|
153
|
+
return this._syntax('unexpected text after the closing "]"');
|
|
154
|
+
}
|
|
155
|
+
if (this._start < 0) {
|
|
156
|
+
// Between elements: whitespace, a comma, the closing bracket, or the
|
|
157
|
+
// first character of a value.
|
|
158
|
+
if (isSpace(c)) { pos++; continue; }
|
|
159
|
+
if (c === 93) {
|
|
160
|
+
if (this._expectValue && this.records > 0) return this._syntax('a trailing comma before "]"');
|
|
161
|
+
this._closed = true; pos++; continue;
|
|
162
|
+
}
|
|
163
|
+
if (c === 44) {
|
|
164
|
+
if (this._expectValue) return this._syntax('two commas with no value between');
|
|
165
|
+
this._expectValue = true; pos++; continue;
|
|
166
|
+
}
|
|
167
|
+
if (!this._expectValue) return this._syntax('a value with no comma before it');
|
|
168
|
+
this._start = pos;
|
|
169
|
+
if (c === 34) {
|
|
170
|
+
const q = stringEnd(s, pos + 1);
|
|
171
|
+
if (q < 0) break; // the string continues in the next chunk
|
|
172
|
+
pos = q + 1;
|
|
173
|
+
const f = this._emit(s.slice(this._start, pos));
|
|
174
|
+
if (f !== null) return f;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (c === 123 || c === 91) { this._depth = 1; pos++; continue; }
|
|
178
|
+
pos++; continue; // a scalar: it runs to a delimiter
|
|
179
|
+
}
|
|
180
|
+
if (this._depth === 0) {
|
|
181
|
+
// A scalar in progress ends at whitespace, a comma or the bracket,
|
|
182
|
+
// which the next round reads.
|
|
183
|
+
if (isSpace(c) || c === 44 || c === 93) {
|
|
184
|
+
const f = this._emit(s.slice(this._start, pos));
|
|
185
|
+
if (f !== null) return f;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
pos++; continue;
|
|
189
|
+
}
|
|
190
|
+
// Inside an object or array element.
|
|
191
|
+
if (c === 34) {
|
|
192
|
+
const q = stringEnd(s, pos + 1);
|
|
193
|
+
if (q < 0) break;
|
|
194
|
+
pos = q + 1; continue;
|
|
195
|
+
}
|
|
196
|
+
if (c === 123 || c === 91) { this._depth++; pos++; continue; }
|
|
197
|
+
if (c === 125 || c === 93) {
|
|
198
|
+
this._depth--; pos++;
|
|
199
|
+
if (this._depth === 0) {
|
|
200
|
+
const f = this._emit(s.slice(this._start, pos));
|
|
201
|
+
if (f !== null) return f;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
pos++;
|
|
206
|
+
}
|
|
207
|
+
// Keep only the element in progress, or nothing.
|
|
208
|
+
if (this._start >= 0) { this._buf = s.slice(this._start); this._pos = pos - this._start; this._start = 0; }
|
|
209
|
+
else { this._buf = ''; this._pos = 0; }
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_syntax (message) {
|
|
214
|
+
this._buf = ''; this._pos = 0; this.done = true;
|
|
215
|
+
return this._fail(parseFailure(this.records, message));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_emit (text) {
|
|
219
|
+
this._start = -1; this._depth = 0; this._expectValue = false;
|
|
220
|
+
const f = this._record(text);
|
|
221
|
+
if (f !== null && this.done) { this._buf = ''; this._pos = 0; return f; }
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
end () {
|
|
226
|
+
if (!this.done) {
|
|
227
|
+
const tail = this._decode(new Uint8Array(0), true);
|
|
228
|
+
if (tail) this.push(tail);
|
|
229
|
+
if (!this.done) {
|
|
230
|
+
if (this._format === 'ndjson') { if (this._buf.trim().length > 0) this._record(this._buf); }
|
|
231
|
+
else if (this._format === 'array') {
|
|
232
|
+
if (!this._opened) this._fail(parseFailure(this.records, 'no JSON array in the input'));
|
|
233
|
+
else if (this._start >= 0 || !this._closed) this._fail(parseFailure(this.records, 'the array is not closed'));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.done = true;
|
|
237
|
+
}
|
|
238
|
+
this._buf = '';
|
|
239
|
+
this.result = { valid: this.failures.length === 0, records: this.records, errors: this.failures };
|
|
240
|
+
return this.result;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function streamValidator (validator, opts) { return new RecordStream(validator, opts); }
|
|
245
|
+
|
|
246
|
+
// A Node http request (http.IncomingMessage) rather than any other Readable.
|
|
247
|
+
// Destroying one at the first failure closes the socket under the response
|
|
248
|
+
// the caller writes next: Node's fetch then gets `write EPIPE` and Chrome
|
|
249
|
+
// "Failed to fetch", never the 400. So reading stops and the rest of the
|
|
250
|
+
// body is drained and dropped; memory stays flat and the answer goes out
|
|
251
|
+
// (Node's fetch read it after 24 ms and 1 MB of a 100 MB upload).
|
|
252
|
+
function isHttpRequest (source) {
|
|
253
|
+
return typeof source.httpVersion === 'string' && typeof source.resume === 'function';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// The whole of an async source (a Node Readable, a Web ReadableStream, any
|
|
257
|
+
// async iterable of chunks) through a record stream. On the first failure
|
|
258
|
+
// the source is stopped: a Node stream is destroyed (a Node http request is
|
|
259
|
+
// drained instead, see isHttpRequest), a Web stream's reader cancelled, an
|
|
260
|
+
// iterator returned, so a failing upload is not validated to its end.
|
|
261
|
+
async function validateStream (validator, source, opts) {
|
|
262
|
+
const rs = new RecordStream(validator, opts);
|
|
263
|
+
if (source && typeof source.getReader === 'function') {
|
|
264
|
+
const reader = source.getReader();
|
|
265
|
+
try {
|
|
266
|
+
for (;;) {
|
|
267
|
+
const { value, done } = await reader.read();
|
|
268
|
+
if (done) break;
|
|
269
|
+
const f = rs.push(value);
|
|
270
|
+
if (f !== null && rs.done) { await reader.cancel('ata: record ' + f.record + ' is invalid'); return rs.end(); }
|
|
271
|
+
}
|
|
272
|
+
} finally { reader.releaseLock(); }
|
|
273
|
+
return rs.end();
|
|
274
|
+
}
|
|
275
|
+
const request = isHttpRequest(source);
|
|
276
|
+
// A request's iterator must leave the stream alive when returned.
|
|
277
|
+
const it = request && typeof source.iterator === 'function' ? source.iterator({ destroyOnReturn: false })
|
|
278
|
+
: source[Symbol.asyncIterator] ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
|
|
279
|
+
for (;;) {
|
|
280
|
+
const { value, done } = await it.next();
|
|
281
|
+
if (done) break;
|
|
282
|
+
const f = rs.push(value);
|
|
283
|
+
if (f !== null && rs.done) {
|
|
284
|
+
if (request) { if (typeof it.return === 'function') await it.return(); source.resume(); }
|
|
285
|
+
else if (typeof source.destroy === 'function') source.destroy();
|
|
286
|
+
else if (typeof it.return === 'function') await it.return();
|
|
287
|
+
return rs.end();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return rs.end();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { RecordStream, streamValidator, validateStream, PARSE_KEYWORD };
|
package/lib/tier0.js
CHANGED
|
@@ -55,8 +55,12 @@ const _PN = typeof Bun === 'undefined' && ({}).__proto__ === Object.prototype; /
|
|
|
55
55
|
function formatFnOf(propSchema, userFormats) {
|
|
56
56
|
const f = propSchema.format;
|
|
57
57
|
if (typeof f !== 'string') return null;
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// Own properties only: `format: 'constructor'` or '__proto__' read an
|
|
59
|
+
// inherited Object member and called it as a check, or threw.
|
|
60
|
+
const own = Object.prototype.hasOwnProperty;
|
|
61
|
+
if (userFormats && own.call(userFormats, f) && typeof userFormats[f] === 'function') return userFormats[f];
|
|
62
|
+
const fc = require('./formats').formatChecks();
|
|
63
|
+
return own.call(fc, f) && typeof fc[f] === 'function' ? fc[f] : null;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
function primConstraint(key, propSchema, userFormats) {
|
package/lib/ts-gen.js
CHANGED
|
@@ -104,25 +104,30 @@ function renderObject(schema, defs, depth) {
|
|
|
104
104
|
}
|
|
105
105
|
return 'Record<string, unknown>';
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Each property's type is rendered once and the index signature below
|
|
108
|
+
// reuses the text; rendering it again there doubled the work per level.
|
|
109
|
+
const types = keys.map((k) => renderValueType(props[k], defs, depth + 1));
|
|
110
|
+
const lines = keys.map((k, i) => {
|
|
109
111
|
const opt = required.has(k) ? '' : '?';
|
|
110
112
|
const safeKey = /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
|
|
111
113
|
const doc = renderJsDoc(props[k], ' ');
|
|
112
|
-
return `${doc} ${safeKey}${opt}: ${
|
|
114
|
+
return `${doc} ${safeKey}${opt}: ${types[i]};`;
|
|
113
115
|
});
|
|
114
116
|
// extra keys when additionalProperties is present as a schema or true
|
|
115
117
|
const extra = schema.additionalProperties;
|
|
116
118
|
if (extra && typeof extra === 'object') {
|
|
117
119
|
// TypeScript requires the index signature to be a supertype of every
|
|
118
120
|
// named property's emitted type. Widen to a union covering each property
|
|
119
|
-
// type, plus undefined when any property is optional.
|
|
121
|
+
// type, plus undefined when any property is optional. A property whose
|
|
122
|
+
// type is an object literal is not repeated in the union: with one at
|
|
123
|
+
// every level the output doubled per level (2^levels characters) and a
|
|
124
|
+
// 22-level chain did not finish. `unknown` covers it.
|
|
120
125
|
const widen = new Set();
|
|
121
126
|
widen.add(renderValueType(extra, defs, depth + 1));
|
|
122
127
|
let hasOptional = false;
|
|
123
|
-
for (
|
|
124
|
-
widen.add(
|
|
125
|
-
if (!required.has(
|
|
128
|
+
for (let i = 0; i < keys.length; i++) {
|
|
129
|
+
widen.add(types[i].includes('{') ? 'unknown' : types[i]);
|
|
130
|
+
if (!required.has(keys[i])) hasOptional = true;
|
|
126
131
|
}
|
|
127
132
|
if (hasOptional) widen.add('undefined');
|
|
128
133
|
const indexType = widen.has('unknown') ? 'unknown' : Array.from(widen).join(' | ');
|
|
@@ -154,12 +159,20 @@ function toTypeName(name) {
|
|
|
154
159
|
// constraints the TypeScript type cannot express (minLength, format, range,
|
|
155
160
|
// etc.). Editors and TypeDoc surface these on hover, so authors can see what
|
|
156
161
|
// the schema requires even though tsc does not enforce it.
|
|
162
|
+
// Text from the schema inside a block comment: a `*/` in a pattern, a format,
|
|
163
|
+
// a default or an example closed the comment and the rest of the value was
|
|
164
|
+
// read as a declaration. The slash is escaped, which a comment reader shows
|
|
165
|
+
// as written.
|
|
166
|
+
function commentSafe(s) {
|
|
167
|
+
return String(s).replace(/\*\//g, '*\\/');
|
|
168
|
+
}
|
|
169
|
+
|
|
157
170
|
function renderJsDoc(schema, indent) {
|
|
158
171
|
if (!schema || typeof schema !== 'object') return '';
|
|
159
172
|
|
|
160
173
|
let description = '';
|
|
161
174
|
if (typeof schema.description === 'string' && schema.description.length > 0) {
|
|
162
|
-
description = schema.description
|
|
175
|
+
description = commentSafe(schema.description);
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
const tags = [];
|
|
@@ -168,15 +181,15 @@ function renderJsDoc(schema, indent) {
|
|
|
168
181
|
for (const k of numKeys) {
|
|
169
182
|
if (typeof schema[k] === 'number') tags.push(`@${k} ${schema[k]}`);
|
|
170
183
|
}
|
|
171
|
-
if (typeof schema.pattern === 'string') tags.push(`@pattern ${schema.pattern}`);
|
|
172
|
-
if (typeof schema.format === 'string') tags.push(`@format ${schema.format}`);
|
|
184
|
+
if (typeof schema.pattern === 'string') tags.push(`@pattern ${commentSafe(schema.pattern)}`);
|
|
185
|
+
if (typeof schema.format === 'string') tags.push(`@format ${commentSafe(schema.format)}`);
|
|
173
186
|
if (schema.uniqueItems === true) tags.push('@uniqueItems');
|
|
174
187
|
if (schema.deprecated === true) tags.push('@deprecated');
|
|
175
188
|
if (schema.default !== undefined) {
|
|
176
|
-
try { tags.push(`@default ${JSON.stringify(schema.default)}`); } catch (_) {}
|
|
189
|
+
try { tags.push(`@default ${commentSafe(JSON.stringify(schema.default))}`); } catch (_) {}
|
|
177
190
|
}
|
|
178
191
|
if (Array.isArray(schema.examples) && schema.examples.length > 0) {
|
|
179
|
-
try { tags.push(`@example ${JSON.stringify(schema.examples[0])}`); } catch (_) {}
|
|
192
|
+
try { tags.push(`@example ${commentSafe(JSON.stringify(schema.examples[0]))}`); } catch (_) {}
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
if (description === '' && tags.length === 0) return '';
|
package/lib/unique-items.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// primitives key a Set or Map by the value itself. Longer arrays that hold an
|
|
11
11
|
// object key a Map by a numeric structural hash, and only items whose hashes
|
|
12
12
|
// collide are compared with `_deq`. Equal values always hash alike: numbers
|
|
13
|
-
// by
|
|
14
|
-
// arrays by position, and objects by the sum of their own key-value hashes,
|
|
13
|
+
// by their bits, with 0 and -0 as one (1 and 1.0 are one double already),
|
|
14
|
+
// strings by their characters, arrays by position, and objects by the sum of their own key-value hashes,
|
|
15
15
|
// whatever the key order. The hash replaced a sorted-key canonical string
|
|
16
16
|
// built for every item on every call, which was over a quarter of the time a
|
|
17
17
|
// generated verdict spent on SchemaStore's sample documents.
|
|
@@ -55,7 +55,15 @@ function _uqh(x) {
|
|
|
55
55
|
if (t === 'number') {
|
|
56
56
|
if (x === 0) return 4;
|
|
57
57
|
h = x | 0;
|
|
58
|
-
|
|
58
|
+
if (h === x) return Math.imul(h, 0x9e3779b1) ^ 5;
|
|
59
|
+
// Every bit of the number goes into the hash, read through a typed
|
|
60
|
+
// array kept on the function (its source is emitted into modules, so
|
|
61
|
+
// nothing outside it is read). Nine decimal places, which this used to
|
|
62
|
+
// keep, hashed every number below 1e-9 alike, and an array of them was
|
|
63
|
+
// compared item against item.
|
|
64
|
+
var f = _uqh.f || (_uqh.f = new Float64Array(1)), w = _uqh.w || (_uqh.w = new Uint32Array(f.buffer));
|
|
65
|
+
f[0] = x;
|
|
66
|
+
return Math.imul(w[0] ^ Math.imul(w[1], 0x9e3779b1), 0x85ebca6b) ^ 5;
|
|
59
67
|
}
|
|
60
68
|
if (t === 'string') {
|
|
61
69
|
h = 0x811c9dc5 ^ x.length;
|
package/lib/validator-core.js
CHANGED
|
@@ -76,6 +76,11 @@ function buildDefaultsApplier(schema) {
|
|
|
76
76
|
return _defaults.buildDefaultsApplier(schema);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// The preprocess passes act on own properties only; what the document
|
|
80
|
+
// inherits is not part of it, and a value found through the prototype belongs
|
|
81
|
+
// to some other object.
|
|
82
|
+
const _hasOwn = Object.prototype.hasOwnProperty;
|
|
83
|
+
|
|
79
84
|
// Build a function that coerces property values to match schema types in-place.
|
|
80
85
|
// Handles string→number, string→integer, string→boolean, number→string, boolean→string.
|
|
81
86
|
function buildCoercer(schema) {
|
|
@@ -109,7 +114,9 @@ function buildNodeCoercer(schema, seen) {
|
|
|
109
114
|
if (!c) continue;
|
|
110
115
|
const { scalar, inner } = c;
|
|
111
116
|
steps.push((o) => {
|
|
112
|
-
|
|
117
|
+
// Own properties only: `key in o` found values the object inherits,
|
|
118
|
+
// and the pass then rewrote the shared object behind them.
|
|
119
|
+
if (!_hasOwn.call(o, key)) return;
|
|
113
120
|
if (scalar) {
|
|
114
121
|
const v = scalar(o[key]);
|
|
115
122
|
if (v !== undefined) o[key] = v;
|
|
@@ -211,7 +218,9 @@ function collectRemovals(schema, actions, path) {
|
|
|
211
218
|
actions.push((data) => {
|
|
212
219
|
let target = data;
|
|
213
220
|
for (let j = 0; j < parentPath.length; j++) {
|
|
214
|
-
|
|
221
|
+
// Own keys only: a nested object the document inherits is not part
|
|
222
|
+
// of it, and deleting from it would strip a shared object.
|
|
223
|
+
if (typeof target !== "object" || target === null || !_hasOwn.call(target, parentPath[j])) return;
|
|
215
224
|
target = target[parentPath[j]];
|
|
216
225
|
}
|
|
217
226
|
if (
|
|
@@ -386,6 +395,11 @@ function _jsonSyntaxRejection(e) {
|
|
|
386
395
|
|
|
387
396
|
function createPaddedBuffer(jsonStr) {
|
|
388
397
|
if (typeof Buffer === 'undefined') throw new Error('createPaddedBuffer requires Node.js Buffer');
|
|
398
|
+
// JSON text only. Buffer.from takes an array-like too, so { length: 1e8 }
|
|
399
|
+
// allocated a hundred million zero bytes and called that the document.
|
|
400
|
+
if (typeof jsonStr !== 'string' && !(jsonStr instanceof Uint8Array)) {
|
|
401
|
+
throw new TypeError('createPaddedBuffer expects a string or a Uint8Array (Buffer) of JSON text');
|
|
402
|
+
}
|
|
389
403
|
const jsonBuf = Buffer.from(jsonStr);
|
|
390
404
|
const padded = Buffer.allocUnsafe(jsonBuf.length + SIMDJSON_PADDING);
|
|
391
405
|
jsonBuf.copy(padded);
|
|
@@ -832,10 +846,11 @@ class Validator {
|
|
|
832
846
|
// and previously invalid data validates. `true` throws here, at
|
|
833
847
|
// construction, with every finding; `'log'` reports through
|
|
834
848
|
// `options.logger` or the console and continues. The check runs on the
|
|
835
|
-
// schema as written, before any normalization touches it.
|
|
849
|
+
// schema as written, before any normalization touches it. A schema given
|
|
850
|
+
// as JSON text is parsed first: the string itself has no keywords.
|
|
836
851
|
if (options.strictSchema === true || options.strictSchema === 'log') {
|
|
837
852
|
const { checkSchemaStrict } = require('./strict-check');
|
|
838
|
-
const problems = checkSchemaStrict(schema, { userKeywords: options.keywords || null });
|
|
853
|
+
const problems = checkSchemaStrict(typeof schema === 'string' ? JSON.parse(schema) : schema, { userKeywords: options.keywords || null });
|
|
839
854
|
if (problems.length > 0) {
|
|
840
855
|
const text = problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n');
|
|
841
856
|
if (options.strictSchema === true) {
|
|
@@ -1586,7 +1601,7 @@ class Validator {
|
|
|
1586
1601
|
// which rebind themselves on first use through _bindEntry, so the
|
|
1587
1602
|
// extension is applied to whatever each one is now and again on rebind.
|
|
1588
1603
|
if (_vx) {
|
|
1589
|
-
this._entryExt = _jsonEntryWrappers(_vx);
|
|
1604
|
+
this._entryExt = _jsonEntryWrappers(this, _vx);
|
|
1590
1605
|
for (const name of ['validateJSON', 'isValidJSON', 'validateAndParse']) {
|
|
1591
1606
|
if (Object.prototype.hasOwnProperty.call(this, name) && typeof this[name] === 'function') _bindEntry(this, name, this[name]);
|
|
1592
1607
|
}
|
|
@@ -1605,34 +1620,13 @@ class Validator {
|
|
|
1605
1620
|
return this._engine || 'interpreter';
|
|
1606
1621
|
}
|
|
1607
1622
|
|
|
1623
|
+
// The native engine's compiled schema and fast slot, set up on the first
|
|
1624
|
+
// buffer call. lib/native-attach.js; the browser build maps it to a stub,
|
|
1625
|
+
// since there is no addon there.
|
|
1608
1626
|
_ensureNative() {
|
|
1609
1627
|
if (this._nativeReady) return;
|
|
1610
1628
|
this._nativeReady = true;
|
|
1611
|
-
|
|
1612
|
-
if (!native) return;
|
|
1613
|
-
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
1614
|
-
let nativeSchemaStr = this._schemaStr;
|
|
1615
|
-
// A boolean root references nothing, so the registry has nowhere to go and
|
|
1616
|
-
// is left out; merging it used to throw on `true` and `false`.
|
|
1617
|
-
if (this._schemaMap.size > 0 && typeof this._schemaObj === 'object' && this._schemaObj !== null) {
|
|
1618
|
-
const merged = JSON.parse(this._schemaStr);
|
|
1619
|
-
if (!merged.$defs) merged.$defs = {};
|
|
1620
|
-
for (const [id, s] of this._schemaMap) {
|
|
1621
|
-
merged.$defs['__ext_' + id.replace(/[^a-zA-Z0-9]/g, '_')] = s;
|
|
1622
|
-
}
|
|
1623
|
-
nativeSchemaStr = JSON.stringify(merged);
|
|
1624
|
-
}
|
|
1625
|
-
this._compiled = new native.CompiledSchema(nativeSchemaStr);
|
|
1626
|
-
// The fast registry is a fixed array of slots in the addon, and registering
|
|
1627
|
-
// a distinct schema past the last one throws. It is an accelerator for the
|
|
1628
|
-
// buffer path, not a requirement, so a full registry leaves the slot at -1
|
|
1629
|
-
// and the buffer methods answer from the JS engine instead. Letting this
|
|
1630
|
-
// throw made every validator built after the 4096th unusable on that path.
|
|
1631
|
-
try {
|
|
1632
|
-
this._fastSlot = native.fastRegister(nativeSchemaStr);
|
|
1633
|
-
} catch {
|
|
1634
|
-
this._fastSlot = -1;
|
|
1635
|
-
}
|
|
1629
|
+
require('./native-attach.js')(this, getNative());
|
|
1636
1630
|
}
|
|
1637
1631
|
|
|
1638
1632
|
// The buffer APIs, installed on the first call to any of them. The schema
|
|
@@ -1653,33 +1647,7 @@ class Validator {
|
|
|
1653
1647
|
if (!native && typeof Buffer === 'undefined') throw new Error(`Native addon required for ${name}(). Use validate(), isValidObject() or validateJSON(), which do not need it.`);
|
|
1654
1648
|
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./buffer-gate.js');
|
|
1655
1649
|
if (!native) return installSlowBufferApis(this);
|
|
1656
|
-
|
|
1657
|
-
this._ensureNative();
|
|
1658
|
-
const slot = this._fastSlot;
|
|
1659
|
-
if (!(slot >= 0)) return installSlowBufferApis(this);
|
|
1660
|
-
const bytes = (b, who) => {
|
|
1661
|
-
if (typeof b === 'string') return Buffer.from(b);
|
|
1662
|
-
if (b instanceof Uint8Array) return b;
|
|
1663
|
-
throw new TypeError(`${who}() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().`);
|
|
1664
|
-
};
|
|
1665
|
-
this.isValid = (b) => native.rawFastValidate(slot, bytes(b, 'isValid'));
|
|
1666
|
-
this.isValidPrepadded = (paddedBuffer, jsonLength) => native.rawFastValidate(slot, paddedBuffer, jsonLength);
|
|
1667
|
-
this.isValidParallel = (b) => native.rawParallelValidate(slot, bytes(b, 'isValidParallel'));
|
|
1668
|
-
this.isValidNDJSON = (b) => native.rawNDJSONValidate(slot, bytes(b, 'isValidNDJSON'));
|
|
1669
|
-
this.countValid = (b) => {
|
|
1670
|
-
const r = native.rawNDJSONValidate(slot, bytes(b, 'countValid'));
|
|
1671
|
-
let n = 0;
|
|
1672
|
-
for (let i = 0; i < r.length; i++) if (r[i]) n++;
|
|
1673
|
-
return n;
|
|
1674
|
-
};
|
|
1675
|
-
this.batchIsValid = (buffers) => {
|
|
1676
|
-
let n = 0;
|
|
1677
|
-
for (const b of buffers) {
|
|
1678
|
-
if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
|
|
1679
|
-
if (native.rawFastValidate(slot, b)) n++;
|
|
1680
|
-
}
|
|
1681
|
-
return n;
|
|
1682
|
-
};
|
|
1650
|
+
require('./native-attach.js').installFast(this, native, bufferNeedsSlowPath, installSlowBufferApis);
|
|
1683
1651
|
}
|
|
1684
1652
|
|
|
1685
1653
|
addSchema(schema) {
|
|
@@ -1696,6 +1664,10 @@ class Validator {
|
|
|
1696
1664
|
const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
|
|
1697
1665
|
this._ownSchemaMap()
|
|
1698
1666
|
this._schemaMap.set(normalized.$id, normalized)
|
|
1667
|
+
// An interpreted twin built for the first calls (lib/codegen-engine.js
|
|
1668
|
+
// coldTwin) carries the registry as it was; the next call builds one
|
|
1669
|
+
// from the registry as it is now.
|
|
1670
|
+
if (this._twin !== undefined) this._twin = undefined
|
|
1699
1671
|
}
|
|
1700
1672
|
|
|
1701
1673
|
// buildSchemaMap hands the same map to every validator built from the same
|
|
@@ -1754,23 +1726,38 @@ class Validator {
|
|
|
1754
1726
|
v._preprocess = preprocess;
|
|
1755
1727
|
|
|
1756
1728
|
// Error function — use pre-compiled from standalone if available, else compile
|
|
1757
|
-
|
|
1729
|
+
const stub = (d) => ({
|
|
1758
1730
|
valid: false,
|
|
1759
1731
|
errors: [
|
|
1760
1732
|
{ code: "validation_failed", path: "", message: "validation failed" },
|
|
1761
1733
|
],
|
|
1762
1734
|
});
|
|
1735
|
+
let errFn = stub;
|
|
1763
1736
|
if (mod.errFn) {
|
|
1764
1737
|
errFn = (d) => mod.errFn(d, true);
|
|
1765
1738
|
} else if (compileToJSCodegenWithErrors !== undefined) {
|
|
1766
1739
|
// Without the code generator (the edge entry) a module that carries no
|
|
1767
|
-
// error function reports the stub above.
|
|
1768
|
-
|
|
1740
|
+
// error function reports the stub above. The function is compiled from
|
|
1741
|
+
// the schema as the runtime reads it, normalized, with the registry and
|
|
1742
|
+
// the caller's formats: compiled from the schema as written, a draft-04
|
|
1743
|
+
// boolean exclusiveMinimum was an annotation and the function said
|
|
1744
|
+
// valid. It only explains a rejection: the module's verdict stands, so
|
|
1745
|
+
// a valid: true from here is reported as the stub, never as acceptance.
|
|
1746
|
+
let jsErrFn = null;
|
|
1747
|
+
try {
|
|
1748
|
+
const draft = schemaObj && typeof schemaObj === 'object' ? draftCode(schemaObj.$schema) : 0;
|
|
1749
|
+
const normalized = _normalizeCallerSchema(schemaObj, 0);
|
|
1750
|
+
const map = buildSchemaMap(options.schemas, draft);
|
|
1751
|
+
jsErrFn = compileToJSCodegenWithErrors(normalized, map && map.size > 0 ? map : null, options.formats || null);
|
|
1752
|
+
if (jsErrFn) jsErrFn({}, true);
|
|
1753
|
+
} catch {
|
|
1754
|
+
jsErrFn = null;
|
|
1755
|
+
}
|
|
1769
1756
|
if (jsErrFn) {
|
|
1770
|
-
|
|
1771
|
-
jsErrFn(
|
|
1772
|
-
|
|
1773
|
-
}
|
|
1757
|
+
errFn = (d) => {
|
|
1758
|
+
const r = jsErrFn(d, true);
|
|
1759
|
+
return r && r.valid === false ? r : stub(d);
|
|
1760
|
+
};
|
|
1774
1761
|
}
|
|
1775
1762
|
}
|
|
1776
1763
|
|
|
@@ -2083,8 +2070,16 @@ function _bindEntry(self, name, fn) {
|
|
|
2083
2070
|
|
|
2084
2071
|
// The JSON entry points under an extension: the schema answers first, and only
|
|
2085
2072
|
// text it accepts is parsed for the check, so a rejection costs nothing extra.
|
|
2086
|
-
|
|
2087
|
-
|
|
2073
|
+
// The check sees the document the schema checked: the parsed text after the
|
|
2074
|
+
// preprocess pass (defaults, coercion, removal). Checking the raw text let a
|
|
2075
|
+
// default the check rejects through.
|
|
2076
|
+
function _jsonEntryWrappers(self, { check, errors }) {
|
|
2077
|
+
const parse = (text) => {
|
|
2078
|
+
const data = JSON.parse(typeof text === 'string' ? text : new TextDecoder().decode(text));
|
|
2079
|
+
const pre = self._preprocess;
|
|
2080
|
+
if (pre !== null && pre !== undefined) pre(data);
|
|
2081
|
+
return data;
|
|
2082
|
+
};
|
|
2088
2083
|
return {
|
|
2089
2084
|
validateJSON: (inner) => (text) => {
|
|
2090
2085
|
const res = inner(text);
|