ata-validator 1.61.2 → 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 +159 -37
- package/bin/ata.js +35 -2
- package/compat.js +15 -2
- package/compiled-standard.d.ts +6 -0
- package/compiled-standard.mjs +66 -0
- 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-standard.js +33 -0
- 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/schema-diff.js +103 -0
- 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 +24 -8
- package/stream.d.ts +51 -0
- package/stream.js +3 -0
- package/stream.mjs +3 -0
package/lib/aot-impl.js
CHANGED
|
@@ -160,13 +160,19 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
160
160
|
// 'inject' writes no function source. The module exports `setFormats(map)`
|
|
161
161
|
// and each format is looked up from that registry when a value is checked,
|
|
162
162
|
// so the caller supplies the functions at load time.
|
|
163
|
+
// The format name a helper name was made from (js-compiler.js ufName):
|
|
164
|
+
// `_uf_` and the name with every character outside [A-Za-z0-9_] written as
|
|
165
|
+
// `$` and four hex digits.
|
|
166
|
+
function formatFromName(name) {
|
|
167
|
+
return name.slice(4).replace(/\$([0-9a-f]{4})/g, (m, h) => String.fromCharCode(parseInt(h, 16)));
|
|
168
|
+
}
|
|
163
169
|
function emitFormatDecls(closures, mode, declKW) {
|
|
164
170
|
if (!closures || closures.length === 0) return { decls: '', exportsSetFormats: false };
|
|
165
171
|
if (mode === 'inject') {
|
|
166
172
|
let out = `${declKW} __formats = Object.create(null);\n`;
|
|
167
173
|
out += `function setFormats(map) { for (const k in map) __formats[k] = map[k]; }\n`;
|
|
168
174
|
for (const { name, format } of closures) {
|
|
169
|
-
const key = JSON.stringify(format === null ? name
|
|
175
|
+
const key = JSON.stringify(format === null ? formatFromName(name) : format);
|
|
170
176
|
out += `${declKW} ${name} = function (v) { const f = __formats[${key}]; if (typeof f !== 'function') throw new Error('ata: format ' + ${key} + ' is not registered; call setFormats({ [' + ${key} + ']: fn }) before validating'); return f(v); };\n`;
|
|
171
177
|
}
|
|
172
178
|
return { decls: out, exportsSetFormats: true };
|
|
@@ -199,10 +205,15 @@ function sourceFrameDecls(frames, schemaFile) {
|
|
|
199
205
|
const texts = [];
|
|
200
206
|
const textIndex = new Map();
|
|
201
207
|
const rows = [];
|
|
208
|
+
// The rows come from the caller's `sourceMap` option. A line or column that
|
|
209
|
+
// is not an integer is written as 0: written as given, a string in one ran
|
|
210
|
+
// as code when the module loaded.
|
|
211
|
+
const int = (n) => (Number.isInteger(n) ? n : 0);
|
|
202
212
|
for (const f of frames.values()) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
213
|
+
const text = typeof f.text === 'string' ? f.text : '';
|
|
214
|
+
let i = textIndex.get(text);
|
|
215
|
+
if (i === undefined) { i = texts.length; texts.push(text); textIndex.set(text, i); }
|
|
216
|
+
rows[f.index] = `[${int(f.line)},${int(f.col)},${i}]`;
|
|
206
217
|
}
|
|
207
218
|
return `const __ataSF = ${JSON.stringify(schemaFile)};\n` +
|
|
208
219
|
`const __ataSL = ${JSON.stringify(texts)};\n` +
|
|
@@ -367,7 +378,27 @@ function defaultsDecl(validator) {
|
|
|
367
378
|
if (defaults.buildDefaultsApplier(schema) === null) return '';
|
|
368
379
|
const embedded = require('./schema-strip').stripAnnotationsKeepingDefaults(schema);
|
|
369
380
|
const text = JSON.stringify(JSON.stringify(embedded)).replace(/</g, '\\u003c');
|
|
370
|
-
|
|
381
|
+
// patternProperties keys are tested as validation tests them: on the
|
|
382
|
+
// linear engine where validation uses it, the platform RegExp otherwise.
|
|
383
|
+
// Called without a matcher, the embedded pass took the RegExp for every
|
|
384
|
+
// pattern, and a key that backtracks cost seconds there. A module whose
|
|
385
|
+
// declaration names __ataSafeRe carries the engine (callers check).
|
|
386
|
+
const { useSafeEngine } = require('./js-compiler')._internal;
|
|
387
|
+
const safe = new Set();
|
|
388
|
+
const seen = new Set();
|
|
389
|
+
const walk = (n) => {
|
|
390
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return;
|
|
391
|
+
seen.add(n);
|
|
392
|
+
if (!Array.isArray(n) && n.patternProperties && typeof n.patternProperties === 'object' && !Array.isArray(n.patternProperties)) {
|
|
393
|
+
for (const k of Object.keys(n.patternProperties)) if (useSafeEngine(k)) safe.add(k);
|
|
394
|
+
}
|
|
395
|
+
for (const k of Object.keys(n)) walk(n[k]);
|
|
396
|
+
};
|
|
397
|
+
walk(embedded);
|
|
398
|
+
const build = safe.size === 0
|
|
399
|
+
? 'buildDefaultsApplier'
|
|
400
|
+
: `(function () { const SAFE = new Set(${JSON.stringify([...safe])}); return (s) => buildDefaultsApplier(s, (p) => SAFE.has(p) ? __ataSafeRe(p) : keyPattern(p)); })()`;
|
|
401
|
+
return `const _ataDefaults = (function () {\nconst MAX_DEPTH = ${defaults.MAX_DEPTH};\n${loadAotOrder().take('defaults.js', defaults.EMBED)}\nreturn ${build};\n})();\n` +
|
|
371
402
|
`let _ataFill;\nfunction applyDefaults(d) { (_ataFill || (_ataFill = _ataDefaults(JSON.parse(${text}))))(d); }\n`;
|
|
372
403
|
}
|
|
373
404
|
|
|
@@ -577,7 +608,7 @@ function toStandaloneModule(validator, opts) {
|
|
|
577
608
|
// which carries it anyway, instead of embedding a copy (2.9 KB gzipped,
|
|
578
609
|
// minified) in every module that has a pattern the platform RegExp does not
|
|
579
610
|
// run.
|
|
580
|
-
|
|
611
|
+
let usesSafeRe = errorRuntime !== null && ((jsFn && jsFn._usesSafeRe) || (jsErrFn && jsErrFn._usesSafeRe));
|
|
581
612
|
let compactCore = '';
|
|
582
613
|
if (errorRuntime !== null) {
|
|
583
614
|
const { stripAnnotations } = require('./schema-strip');
|
|
@@ -649,9 +680,33 @@ function toStandaloneModule(validator, opts) {
|
|
|
649
680
|
// safe-regex prelude, which has a module-scope parse() of its own for
|
|
650
681
|
// reading patterns. A second declaration of that name shadowed it and the
|
|
651
682
|
// prelude called this one instead, reaching _fn before its initialiser ran.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
683
|
+
// With a pass before the check (the validator's coerceTypes or
|
|
684
|
+
// removeAdditional, and its defaults unless useDefaults is off), the
|
|
685
|
+
// runtime's parse() takes its verdict on a filled copy of the input and
|
|
686
|
+
// checks the copy it hands back too (buildParse in lib/codegen-engine.js).
|
|
687
|
+
// Checked on the raw input, a default that makes a conditional apply was
|
|
688
|
+
// missed and parse() returned what validate() refuses after the fill.
|
|
689
|
+
const fillDecl = defaultsDecl(validator);
|
|
690
|
+
const fills = !!fillDecl && vopts.useDefaults !== false;
|
|
691
|
+
// The defaults pass tests patternProperties keys on the linear engine
|
|
692
|
+
// where validation does; the module then carries the engine.
|
|
693
|
+
const fillSafeRe = fillDecl.includes('__ataSafeRe(');
|
|
694
|
+
if (fillSafeRe && errorRuntime !== null) usesSafeRe = true;
|
|
695
|
+
const parseCore = !cloneExpr
|
|
696
|
+
? ''
|
|
697
|
+
: !fills && pre.decl === ''
|
|
698
|
+
? `function _ataParse(data) {\n if (!_fn(data)) { const e = new Error('validation failed'); e.name = 'AtaValidationError'; throw e; }\n return ${cloneExpr};\n}\n`
|
|
699
|
+
: `function _ataParse(data) {
|
|
700
|
+
let doc;
|
|
701
|
+
try { doc = structuredClone(data); } catch (_) { doc = undefined; }
|
|
702
|
+
if (doc === undefined && data !== undefined) throw new TypeError('parse() is not available for this value: it cannot be copied to fill its defaults before the check. Use validate() instead.');
|
|
703
|
+
if (doc !== null && typeof doc === 'object') { ${pre.decl !== '' ? '_ataPre(doc); ' : ''}${fills ? 'applyDefaults(doc); ' : ''}}
|
|
704
|
+
if (!_fn(doc)) { const e = new Error('validation failed'); e.name = 'AtaValidationError'; throw e; }
|
|
705
|
+
const out = ${cloneExpr};
|
|
706
|
+
if (!_fn(out)) 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.');
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
709
|
+
`;
|
|
655
710
|
|
|
656
711
|
// validateJSON(): parse the text, validate, and on failure attach a
|
|
657
712
|
// dataFrame (byte offset, line, col, source line) to every error by
|
|
@@ -666,7 +721,7 @@ function toStandaloneModule(validator, opts) {
|
|
|
666
721
|
// the worst case of a pointer at the very end. It is also 125 characters
|
|
667
722
|
// smaller than the full-map builder it replaces.
|
|
668
723
|
const positionsCore = !(opts && opts.positions) ? '' : `const _ataPosMap = ${buildTargetedPositionMap.toString()};
|
|
669
|
-
function _ataFrame(p) {
|
|
724
|
+
function _ataFrame(p) { const f = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text }; if (p.textCol !== undefined) f.textCol = p.textCol; return f; }
|
|
670
725
|
function validateJSON(text) {
|
|
671
726
|
const s = String(text);
|
|
672
727
|
let data;
|
|
@@ -705,7 +760,6 @@ function validateJSON(text) {
|
|
|
705
760
|
const hashDecl = `const schemaHash = ${JSON.stringify(hashSrc)};\nconst ataVersion = ${JSON.stringify(ATA_VERSION)};\n`;
|
|
706
761
|
|
|
707
762
|
let names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
|
|
708
|
-
const fillDecl = defaultsDecl(validator);
|
|
709
763
|
if (fillDecl) names += ', applyDefaults';
|
|
710
764
|
names += ', schemaHash, ataVersion';
|
|
711
765
|
if (positionsCore) names += ', validateJSON';
|
|
@@ -726,7 +780,7 @@ ${errorRuntime === null ? '// Schema is embedded; runtime has zero dependency on
|
|
|
726
780
|
// Re-run the build after upgrading ata: the version above is what wrote this.
|
|
727
781
|
${degradedNote}'use strict';
|
|
728
782
|
${errorRuntime === null ? '' : format === 'esm' ? `import { createErrors as _ataCreateErrors${usesSafeRe ? ', compileSafe as _ataCompileSafe' : ''}${usesUriWalk ? ', uriWalk as _ataUriWalk' : ''} } from ${JSON.stringify(errorRuntime)};\n` : `const { createErrors: _ataCreateErrors${usesSafeRe ? ', compileSafe: _ataCompileSafe' : ''}${usesUriWalk ? ', uriWalk: _ataUriWalk' : ''} } = require(${JSON.stringify(errorRuntime)});\n`}${_CP_LEN_SOURCE}
|
|
729
|
-
${errorRuntime !== null ? (usesSafeRe ? 'const __ataSafeRe = _ataCompileSafe;\n' : '') : safeRePrelude(jsFn, jsErrFn, onePass !== null && onePass.usesSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
783
|
+
${errorRuntime !== null ? (usesSafeRe ? 'const __ataSafeRe = _ataCompileSafe;\n' : '') : safeRePrelude(jsFn, jsErrFn, (onePass !== null && onePass.usesSafeRe) || fillSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
730
784
|
const ABORT = Object.freeze({
|
|
731
785
|
valid: false,
|
|
732
786
|
errors: Object.freeze([Object.freeze({
|
|
@@ -885,7 +939,10 @@ function toSchemaModule(validator, opts) {
|
|
|
885
939
|
? `import { createValidator as _ataCreateValidator } from ${spec};\n`
|
|
886
940
|
: `const { createValidator: _ataCreateValidator } = require(${spec});\n`)
|
|
887
941
|
+ (regSpec === null ? '' : format === 'esm' ? `import _ataRegistry from ${regSpec};\n` : `const _ataRegistry = require(${regSpec});\n`);
|
|
888
|
-
|
|
942
|
+
let fillDecl = defaultsDecl(validator);
|
|
943
|
+
// No verdict code here to carry the linear engine; the defaults pass
|
|
944
|
+
// brings its own when it tests keys on it.
|
|
945
|
+
if (fillDecl.includes('__ataSafeRe(')) fillDecl = getSafeRegexEmbed() + '\n' + fillDecl;
|
|
889
946
|
const expNames = 'validate, isValid' + (fillDecl ? ', applyDefaults' : '') + ', schemaHash, ataVersion';
|
|
890
947
|
const exp = format === 'esm'
|
|
891
948
|
? `export { ${expNames} };\nexport default { ${expNames} };\n`
|
|
@@ -1035,6 +1092,7 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
1035
1092
|
// The runtime's passes over the data, for the options asked for, and its
|
|
1036
1093
|
// defaults pass as the validator's applyDefaults.
|
|
1037
1094
|
const fillDecl = defaultsDecl(v);
|
|
1095
|
+
if (fillDecl.includes('__ataSafeRe(')) bundleUsesSafeRe = true;
|
|
1038
1096
|
const pre = bundlePreprocess(v, opts, fillDecl, 'bundleStandalone');
|
|
1039
1097
|
preamble = preamble ? `${preamble}\n${fillDecl}${pre.decl}` : fillDecl + pre.decl;
|
|
1040
1098
|
const ret = (body) => fillDecl
|
|
@@ -1188,13 +1246,21 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
1188
1246
|
// User format functions are referenced as _uf_<name> by the hybrid and error
|
|
1189
1247
|
// bodies. Collect them across all schemas (deduped by name) and embed via
|
|
1190
1248
|
// Function#toString so the bundle stays self-contained.
|
|
1191
|
-
|
|
1249
|
+
// One declaration per helper name. The name is one to one with the
|
|
1250
|
+
// format name, so two entries under one name are the same format; were
|
|
1251
|
+
// they bound to different functions the bundle could not serve both, so
|
|
1252
|
+
// that is refused rather than letting one schema run another's checker.
|
|
1253
|
+
const fmtSeen = new Map();
|
|
1192
1254
|
const fmtAll = [];
|
|
1193
1255
|
for (const e of entries) {
|
|
1194
1256
|
if (!e || !e.fmt) continue;
|
|
1195
1257
|
for (const entry of e.fmt) {
|
|
1196
|
-
|
|
1197
|
-
|
|
1258
|
+
const prev = fmtSeen.get(entry.name);
|
|
1259
|
+
if (prev !== undefined) {
|
|
1260
|
+
if (prev !== entry.fn) throw new Error(`ata: custom format "${entry.format === null ? formatFromName(entry.name) : entry.format}" is bound to two different functions in this bundle`);
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
fmtSeen.set(entry.name, entry.fn);
|
|
1198
1264
|
fmtAll.push(entry);
|
|
1199
1265
|
}
|
|
1200
1266
|
}
|
|
@@ -1255,9 +1321,24 @@ function loadBundle(Validator, mods, schemas, opts) {
|
|
|
1255
1321
|
// do not write but a pattern could carry, has its backslash dropped, since
|
|
1256
1322
|
// `\x3C` already is the escape. A module with neither sequence, which is
|
|
1257
1323
|
// nearly every one, comes back as the same string.
|
|
1324
|
+
// The match is the `<` and what follows; the backslashes before it are
|
|
1325
|
+
// counted from the match offset. Matching them as part of the pattern made
|
|
1326
|
+
// the scan re-read a run of backslashes at every `<` after it, so an enum
|
|
1327
|
+
// value of 40000 backslashes and `<a` took 6.5 s. One pass, whatever the run.
|
|
1258
1328
|
function htmlSafe(src) {
|
|
1259
1329
|
if (typeof src !== 'string' || src.indexOf('<') === -1) return src;
|
|
1260
|
-
|
|
1330
|
+
let out = '';
|
|
1331
|
+
let last = 0;
|
|
1332
|
+
const re = /<(\/script|!--)/gi;
|
|
1333
|
+
let m;
|
|
1334
|
+
while ((m = re.exec(src)) !== null) {
|
|
1335
|
+
const at = m.index;
|
|
1336
|
+
let bs = 0;
|
|
1337
|
+
while (at - bs - 1 >= last && src.charCodeAt(at - bs - 1) === 92) bs++;
|
|
1338
|
+
out += src.slice(last, at - (bs % 2)) + '\\x3C' + m[1];
|
|
1339
|
+
last = at + m[0].length;
|
|
1340
|
+
}
|
|
1341
|
+
return last === 0 ? src : out + src.slice(last);
|
|
1261
1342
|
}
|
|
1262
1343
|
const htmlSafeResult = (fn) => function () { return htmlSafe(fn.apply(this, arguments)); };
|
|
1263
1344
|
|
|
@@ -1278,6 +1359,16 @@ module.exports = {
|
|
|
1278
1359
|
if (!Array.isArray(schemas)) return er.errorRuntimeSource(format, ATA_VERSION, undefined, !!schemaMode);
|
|
1279
1360
|
const { stripAnnotations } = require('./schema-strip');
|
|
1280
1361
|
const embedded = schemas.map((x) => stripAnnotations(x && typeof x === 'object' && x._schemaObj !== undefined ? x._schemaObj : x));
|
|
1362
|
+
// A module reaches the registered schemas (`schemas` option) through
|
|
1363
|
+
// $ref, so what they use the runtime must carry too. Read from the
|
|
1364
|
+
// validator's map, since the validator may not have compiled yet: a
|
|
1365
|
+
// pattern that lived only in a registered schema was missed, the runtime
|
|
1366
|
+
// went out without the pattern engine, and the module matched on the
|
|
1367
|
+
// platform RegExp.
|
|
1368
|
+
for (const x of schemas) {
|
|
1369
|
+
if (!x || typeof x !== 'object' || !(x._schemaMap instanceof Map)) continue;
|
|
1370
|
+
for (const ext of x._schemaMap.values()) if (ext && typeof ext === 'object') embedded.push(stripAnnotations(ext));
|
|
1371
|
+
}
|
|
1281
1372
|
const features = er.runtimeFeatures(embedded);
|
|
1282
1373
|
for (const x of schemas) if (x && x._jsFn && x._jsFn._usesSafeRe) features['safe-regex'] = true;
|
|
1283
1374
|
return er.errorRuntimeSource(format, ATA_VERSION, features, !!schemaMode);
|
package/lib/buffer-gate.js
CHANGED
|
@@ -24,11 +24,16 @@ const UNSUPPORTED_KEYWORDS = new Set([
|
|
|
24
24
|
'contains', 'minContains', 'maxContains',
|
|
25
25
|
'unevaluatedProperties', 'unevaluatedItems',
|
|
26
26
|
'dependencies', 'dependentSchemas', 'dependentRequired',
|
|
27
|
-
'propertyNames', 'patternProperties',
|
|
27
|
+
'propertyNames', 'patternProperties', 'propertyDependencies',
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
|
-
// Formats the native
|
|
31
|
-
|
|
30
|
+
// Formats the native engine checks by the same rules as lib/formats.js,
|
|
31
|
+
// held to the same answers by tests/test_native_formats.js. Every other
|
|
32
|
+
// format name goes through validate(): the native engine has no check for
|
|
33
|
+
// it and would let any string pass. This was a list of the divergent ones,
|
|
34
|
+
// which missed every format the engine does not know (json-pointer, iri,
|
|
35
|
+
// uri-template, regex and the rest) and a caller's own formats.
|
|
36
|
+
const NATIVE_FORMATS = new Set(['email', 'date', 'uri', 'uuid', 'ipv4', 'ipv6', 'hostname']);
|
|
32
37
|
|
|
33
38
|
const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies'];
|
|
34
39
|
const SUBSCHEMA_NODES = [
|
|
@@ -92,7 +97,7 @@ function refersBack(root) {
|
|
|
92
97
|
return visit(root);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
function walk(schema, depth) {
|
|
100
|
+
function walk(schema, depth, userFormats) {
|
|
96
101
|
if (depth > NATIVE_DEPTH) return true;
|
|
97
102
|
if (schema === true || schema === false) {
|
|
98
103
|
// A boolean root is answered wrong by the walker; nested booleans are
|
|
@@ -101,7 +106,7 @@ function walk(schema, depth) {
|
|
|
101
106
|
}
|
|
102
107
|
if (schema === null || typeof schema !== 'object') return false;
|
|
103
108
|
if (Array.isArray(schema)) {
|
|
104
|
-
for (const s of schema) if (walk(s, depth + 1)) return true;
|
|
109
|
+
for (const s of schema) if (walk(s, depth + 1, userFormats)) return true;
|
|
105
110
|
return false;
|
|
106
111
|
}
|
|
107
112
|
for (const key of Object.keys(schema)) {
|
|
@@ -114,7 +119,7 @@ function walk(schema, depth) {
|
|
|
114
119
|
if (key === '$id' && depth > 0) return true;
|
|
115
120
|
// An empty enum rejects everything; the walker accepts everything.
|
|
116
121
|
if (key === 'enum' && Array.isArray(v) && v.length === 0) return true;
|
|
117
|
-
if (key === 'format' &&
|
|
122
|
+
if (key === 'format' && (!NATIVE_FORMATS.has(v) || (userFormats && Object.prototype.hasOwnProperty.call(userFormats, v)))) return true;
|
|
118
123
|
// Unicode property escapes: RE2 cannot parse them and the walker then
|
|
119
124
|
// skips the pattern instead of failing.
|
|
120
125
|
if (key === 'pattern' && typeof v === 'string' && /\\[pP]\{/.test(v)) return true;
|
|
@@ -126,10 +131,10 @@ function walk(schema, depth) {
|
|
|
126
131
|
|
|
127
132
|
if (SUBSCHEMA_MAPS.includes(key)) {
|
|
128
133
|
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
129
|
-
for (const k of Object.keys(v)) if (walk(v[k], depth + 1)) return true;
|
|
134
|
+
for (const k of Object.keys(v)) if (walk(v[k], depth + 1, userFormats)) return true;
|
|
130
135
|
}
|
|
131
136
|
} else if (SUBSCHEMA_NODES.includes(key)) {
|
|
132
|
-
if (walk(v, depth + 1)) return true;
|
|
137
|
+
if (walk(v, depth + 1, userFormats)) return true;
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
return false;
|
|
@@ -138,13 +143,15 @@ function walk(schema, depth) {
|
|
|
138
143
|
// True when the buffer APIs must answer through validate() for this schema.
|
|
139
144
|
// `keywords` is the validator's normalized custom keyword map, if any: the
|
|
140
145
|
// native walker knows nothing about those, so a schema using one is routed
|
|
141
|
-
// through validate() like the other shapes here.
|
|
142
|
-
|
|
146
|
+
// through validate() like the other shapes here. `userFormats` is the
|
|
147
|
+
// validator's `formats` option: a format it defines, even under a built-in
|
|
148
|
+
// name, is a check only the JS engines run.
|
|
149
|
+
function bufferNeedsSlowPath(schema, schemaMap, keywords, userFormats) {
|
|
143
150
|
if (keywords && schemaUsesKeywords(schema, keywords)) return true;
|
|
144
|
-
if (walk(schema, 0)) return true;
|
|
151
|
+
if (walk(schema, 0, userFormats)) return true;
|
|
145
152
|
if (refersBack(schema)) return true;
|
|
146
153
|
if (schemaMap && schemaMap.size > 0) {
|
|
147
|
-
for (const s of schemaMap.values()) if (walk(s, 1)) return true;
|
|
154
|
+
for (const s of schemaMap.values()) if (walk(s, 1, userFormats)) return true;
|
|
148
155
|
}
|
|
149
156
|
return false;
|
|
150
157
|
}
|
|
@@ -163,8 +170,12 @@ function toText(input, name) {
|
|
|
163
170
|
function installSlowBufferApis(validator) {
|
|
164
171
|
const isValidText = (text) => validator.isValidJSON(text);
|
|
165
172
|
validator.isValid = (input) => isValidText(toText(input, 'isValid'));
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
// The length is the caller's, so it is held to the buffer: a view past its
|
|
174
|
+
// end read the bytes of whatever shared the pool, and answered on them.
|
|
175
|
+
validator.isValidPrepadded = (paddedBuffer, jsonLength) => {
|
|
176
|
+
if (!(paddedBuffer instanceof Uint8Array) || !Number.isInteger(jsonLength) || jsonLength < 0 || jsonLength > paddedBuffer.byteLength) return false;
|
|
177
|
+
return isValidText(Buffer.from(paddedBuffer.buffer, paddedBuffer.byteOffset, jsonLength).toString('utf8'));
|
|
178
|
+
};
|
|
168
179
|
const ndjson = (input, name) => {
|
|
169
180
|
const lines = toText(input, name).split('\n');
|
|
170
181
|
const out = [];
|
package/lib/bun-target.js
CHANGED
|
@@ -79,15 +79,20 @@ function empty () { return { string: [], number: [], boolean: [], null: [], obje
|
|
|
79
79
|
function universal () { const t = empty(); for (const k of KINDS) t[k].push(UNIVERSAL[k]); return t }
|
|
80
80
|
function single (kind, alt) { const t = empty(); t[kind].push(alt); return t }
|
|
81
81
|
function union (a, b) { const t = empty(); for (const k of [...KINDS, 'alias']) t[k] = dedupe([...a[k], ...b[k]]); return t }
|
|
82
|
-
function dedupe (xs) { return xs.
|
|
82
|
+
function dedupe (xs) { return xs.length < 2 ? xs : [...new Set(xs)] }
|
|
83
83
|
function isUniversal (t) { return t.alias.length === 0 && KINDS.every((k) => t[k].length === 1 && t[k][0] === UNIVERSAL[k]) }
|
|
84
84
|
|
|
85
85
|
// Intersection, or null where the two cannot be written as one type; the
|
|
86
|
-
// caller then keeps one side in the residual.
|
|
86
|
+
// caller then keeps one side in the residual. The product of two unions
|
|
87
|
+
// distributes, so allOf over n branches of two shapes each is 2^n
|
|
88
|
+
// alternatives: 14 branches took 2.3 s and 16 did not finish. Past this many
|
|
89
|
+
// alternatives for one kind the branch stays in the residual.
|
|
90
|
+
const MAX_ALTERNATIVES = 256
|
|
87
91
|
function intersect (a, b) {
|
|
88
92
|
if (a.alias.length || b.alias.length) return null
|
|
89
93
|
const t = empty()
|
|
90
94
|
for (const k of KINDS) {
|
|
95
|
+
if (a[k].length * b[k].length > MAX_ALTERNATIVES) return null
|
|
91
96
|
for (const x of a[k]) {
|
|
92
97
|
for (const y of b[k]) {
|
|
93
98
|
const z = meet(k, x, y)
|
|
@@ -548,17 +553,28 @@ function schemaToBunType (root, opts = {}) {
|
|
|
548
553
|
// the type, and the module's functions with the type on them. `parse`
|
|
549
554
|
// declares the type where it parses, which is what the compiler reads to make
|
|
550
555
|
// the objects with that layout; the module then checks the content.
|
|
556
|
+
// A string as a JavaScript literal with every line terminator escaped.
|
|
557
|
+
// JSON.stringify leaves U+2028 and U+2029 as they are, and inside a `//`
|
|
558
|
+
// comment those end the comment the way a line feed does.
|
|
559
|
+
function jsString (s) {
|
|
560
|
+
return JSON.stringify(String(s)).replace(/[\u2028\u2029]/g, (c) => '\\u' + c.charCodeAt(0).toString(16))
|
|
561
|
+
}
|
|
562
|
+
// The same without the quotes, for text that goes into a // comment: a file
|
|
563
|
+
// name holding a line break ended the comment and the rest of the name was
|
|
564
|
+
// source.
|
|
565
|
+
function commentText (s) { return jsString(s).slice(1, -1) }
|
|
566
|
+
|
|
551
567
|
function toBunModule ({ schemaFile, moduleImport, typeName: tn, typeSource, aliases }) {
|
|
552
568
|
const lines = []
|
|
553
|
-
lines.push(`// Generated by ata build --target bun from ${schemaFile}. Do not edit.`)
|
|
569
|
+
lines.push(`// Generated by ata build --target bun from ${commentText(schemaFile)}. Do not edit.`)
|
|
554
570
|
lines.push('//')
|
|
555
571
|
lines.push('// For executables built with `bun build --compile --aot`: the schema as a type')
|
|
556
|
-
lines.push(`// for the program, with the plain module (${moduleImport}) answering every`)
|
|
572
|
+
lines.push(`// for the program, with the plain module (${commentText(moduleImport)}) answering every`)
|
|
557
573
|
lines.push('// check, so the answers and the errors are the module\'s. `parse(text)` declares')
|
|
558
574
|
lines.push('// the type where it parses and the module checks the content; it is not faster')
|
|
559
575
|
lines.push('// than a plain parse and the module. Everywhere else, including plain `bun run`,')
|
|
560
576
|
lines.push('// the plain module does the same without this file.')
|
|
561
|
-
lines.push(`import * as _m from ${
|
|
577
|
+
lines.push(`import * as _m from ${jsString(moduleImport)}`)
|
|
562
578
|
lines.push('')
|
|
563
579
|
for (const [n, src] of aliases) lines.push(`type ${n} = ${src}`)
|
|
564
580
|
lines.push(`export type ${tn} = ${typeSource}`)
|
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
|
}
|