ata-validator 1.19.0 → 1.20.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/index.js +20 -4
- package/lib/aot-impl.js +116 -21
- package/lib/branch-collapse.js +72 -47
- package/lib/interpreter.js +40 -11
- package/lib/js-compiler.js +53 -24
- package/lib/output-format.js +140 -0
- package/lib/plan-compiler.js +61 -15
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/index.js
CHANGED
|
@@ -1461,9 +1461,18 @@ class Validator {
|
|
|
1461
1461
|
});
|
|
1462
1462
|
this._engine = 'interpreter';
|
|
1463
1463
|
if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1464
|
+
// abortEarly is a documented contract, not a property of whichever engine
|
|
1465
|
+
// answered: it promises the frozen ATA9000 stub instead of a detailed
|
|
1466
|
+
// error, so code that branches on it has to behave the same with and
|
|
1467
|
+
// without code generation. Taking the verdict path here also skips
|
|
1468
|
+
// building the errors the caller said it did not want.
|
|
1469
|
+
const run = options.abortEarly
|
|
1470
|
+
? (preprocess
|
|
1471
|
+
? (data) => { preprocess(data); return interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
|
|
1472
|
+
: (data) => (interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT))
|
|
1473
|
+
: (preprocess
|
|
1474
|
+
? (data) => { preprocess(data); return interp.validate(data); }
|
|
1475
|
+
: (data) => interp.validate(data));
|
|
1467
1476
|
this.validate = run;
|
|
1468
1477
|
this.isValidObject = this._fastVerdict
|
|
1469
1478
|
? this._fastVerdict
|
|
@@ -2039,6 +2048,7 @@ function compile(schema, opts) {
|
|
|
2039
2048
|
const { toTypeScript } = require("./lib/ts-gen");
|
|
2040
2049
|
const { renderPretty } = require("./lib/render-pretty");
|
|
2041
2050
|
const { renderCompact } = require("./lib/render-compact");
|
|
2051
|
+
const { toOutput } = require("./lib/output-format");
|
|
2042
2052
|
const { renderJSON } = require("./lib/render-json");
|
|
2043
2053
|
const { suggestFor } = require("./lib/suggestions");
|
|
2044
2054
|
const { reprValue } = require("./lib/enrich-error");
|
|
@@ -2212,7 +2222,12 @@ _defineLazyMethod('isValidObject', (self) => (data) => {
|
|
|
2212
2222
|
return r;
|
|
2213
2223
|
};
|
|
2214
2224
|
} else {
|
|
2215
|
-
|
|
2225
|
+
// `new Function` is a property of the realm, not of the schema: under a
|
|
2226
|
+
// strict CSP or `--disallow-code-generation-from-strings` this throws
|
|
2227
|
+
// rather than declining, and the EvalError reached the caller of a verdict
|
|
2228
|
+
// method. The full compile can answer without code generation, so fall
|
|
2229
|
+
// through to it. The tier-0 branch above already guarded its own call.
|
|
2230
|
+
try { self._ensureCodegen(); } catch { /* no codegen in this realm */ }
|
|
2216
2231
|
// Codegen can bail on shapes it cannot represent; the full compile
|
|
2217
2232
|
// binds the native path or the unsupported thrower instead of
|
|
2218
2233
|
// leaving this stub to re-dispatch to itself.
|
|
@@ -2263,6 +2278,7 @@ module.exports = {
|
|
|
2263
2278
|
defineSchema,
|
|
2264
2279
|
renderPretty,
|
|
2265
2280
|
renderCompact,
|
|
2281
|
+
toOutput,
|
|
2266
2282
|
renderJSON,
|
|
2267
2283
|
attachSuggestions, // internal: used by the renderers; not public API
|
|
2268
2284
|
};
|
package/lib/aot-impl.js
CHANGED
|
@@ -54,13 +54,26 @@ function safeRePrelude(...fns) {
|
|
|
54
54
|
return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// A validator can enforce more than its schema says. `withKeywords` from
|
|
58
|
+
// @ata-project/keywords wraps an instance's entry points, and a wrapper
|
|
59
|
+
// declares that by setting `_externalChecks`. The emitters below build their
|
|
60
|
+
// module from the compiled schema alone, so a wrapped instance would emit a
|
|
61
|
+
// module that accepts documents the validator itself rejects. Refuse instead.
|
|
62
|
+
// Declining to compile is recoverable. A module that wrongly accepts is not.
|
|
63
|
+
function assertEmittable(validator, fnName) {
|
|
64
|
+
if (validator._usesKeywords) {
|
|
65
|
+
throw new Error(fnName + ': a schema that uses custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
66
|
+
}
|
|
67
|
+
if (validator._externalChecks) {
|
|
68
|
+
throw new Error(fnName + ': this validator enforces checks that are not in its schema, so a standalone module would be weaker than the validator it came from. Those checks come from a wrapper such as withKeywords() from @ata-project/keywords, and JSON Schema has no spelling for them. Compile the unwrapped validator if the extra checks are not needed.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
// --- Standalone pre-compilation ---
|
|
58
73
|
// Generate a JS module string that can be written to a file.
|
|
59
74
|
// On next startup, load with Validator.fromStandalone() -- zero compile time.
|
|
60
75
|
function toStandalone(validator) {
|
|
61
|
-
|
|
62
|
-
throw new Error('toStandalone: a schema that uses custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
63
|
-
}
|
|
76
|
+
assertEmittable(validator, 'toStandalone');
|
|
64
77
|
validator._ensureCompiled();
|
|
65
78
|
const jsFn = validator._jsFn;
|
|
66
79
|
if (!jsFn || !jsFn._source) return null;
|
|
@@ -233,9 +246,7 @@ function emitClone(node, access, depth) {
|
|
|
233
246
|
}
|
|
234
247
|
|
|
235
248
|
function toStandaloneModule(validator, opts) {
|
|
236
|
-
|
|
237
|
-
throw new Error('toStandaloneModule: a schema that uses custom keywords cannot be compiled into a standalone module (option "keywords")');
|
|
238
|
-
}
|
|
249
|
+
assertEmittable(validator, 'toStandaloneModule');
|
|
239
250
|
validator._ensureCompiled();
|
|
240
251
|
const jsFn = validator._jsFn;
|
|
241
252
|
if (!jsFn || !jsFn._source) return null;
|
|
@@ -299,10 +310,39 @@ function toStandaloneModule(validator, opts) {
|
|
|
299
310
|
if (lines.length) closureDecls = lines.join('\n') + '\n';
|
|
300
311
|
}
|
|
301
312
|
|
|
313
|
+
// A few helpers are self-contained and named the same wherever they appear,
|
|
314
|
+
// the email check being the big one at about 2.7 KB. The boolean function
|
|
315
|
+
// and the error function each hoisted their own copy, so every module that
|
|
316
|
+
// used a format carried it twice. They are declared once at module scope
|
|
317
|
+
// here and removed from both bodies. Everything else in the preamble stays
|
|
318
|
+
// where it is: emitConstant and the generated branch checks name things per
|
|
319
|
+
// compilation, and two of them at module scope would collide.
|
|
320
|
+
const sharedDecls = [];
|
|
321
|
+
{
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
for (const fn of [jsFn, jsErrFn]) {
|
|
324
|
+
if (!fn || !fn._sharedHelpers) continue;
|
|
325
|
+
for (const h of fn._sharedHelpers) if (!seen.has(h)) { seen.add(h); sharedDecls.push(h); }
|
|
326
|
+
}
|
|
327
|
+
if (sharedDecls.length && errCore) {
|
|
328
|
+
for (const h of sharedDecls) errCore = errCore.split(h).join('');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const sharedBlock = sharedDecls.length ? sharedDecls.join('\n') + '\n' : '';
|
|
332
|
+
|
|
302
333
|
// Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
|
|
303
334
|
// runtime emits them before the function). The standalone module must declare
|
|
304
335
|
// them at module scope too, or _fn references undefined names (e.g. _af1_b0).
|
|
305
|
-
const
|
|
336
|
+
const sharedSet = new Set(sharedDecls);
|
|
337
|
+
const preambleParts = jsFn._preambleParts
|
|
338
|
+
? jsFn._preambleParts.filter((part) => !sharedSet.has(part))
|
|
339
|
+
: null;
|
|
340
|
+
const preambleBody = preambleParts !== null
|
|
341
|
+
? (preambleParts.length ? preambleParts.join('\n ') + '\n ' : '')
|
|
342
|
+
: (jsFn._preambleSource || '');
|
|
343
|
+
const preambleDecls = preambleBody || (jsFn._preambleGuard || '')
|
|
344
|
+
? (jsFn._preambleParts ? (jsFn._preambleGuard || '') + preambleBody : preambleBody) + '\n'
|
|
345
|
+
: '';
|
|
306
346
|
|
|
307
347
|
// User-supplied format functions are referenced as _uf_<name> by both the
|
|
308
348
|
// boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
|
|
@@ -353,7 +393,7 @@ const ABORT = Object.freeze({
|
|
|
353
393
|
path: '',
|
|
354
394
|
})]),
|
|
355
395
|
});
|
|
356
|
-
${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
396
|
+
${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
357
397
|
${src}
|
|
358
398
|
};
|
|
359
399
|
${errCore}function isValid(data) { return _fn(data); }
|
|
@@ -404,11 +444,14 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
404
444
|
const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
|
|
405
445
|
let bundleUsesSafeRe = false;
|
|
406
446
|
let bundleInjects = false;
|
|
407
|
-
|
|
447
|
+
// Pass one compiles; pass two emits. The helpers that several schemas hoist
|
|
448
|
+
// are the same text declaring the same name, so the bundle keeps one copy at
|
|
449
|
+
// module scope, and pass two needs to know the set before it writes anything.
|
|
450
|
+
const compiledEntries = schemas.map((schema) => {
|
|
408
451
|
const v = new Validator(schema, bundleOpts);
|
|
409
452
|
v._ensureCompiled();
|
|
410
453
|
const jsFn = v._jsFn;
|
|
411
|
-
if (!jsFn || !jsFn._hybridSource) return
|
|
454
|
+
if (!jsFn || !jsFn._hybridSource) return null;
|
|
412
455
|
// The schema the validator compiled, not the one the caller passed. They
|
|
413
456
|
// differ whenever anything prepared the schema: draft-07 normalization,
|
|
414
457
|
// `format` removed under assertFormat: false, keywords outside the
|
|
@@ -420,10 +463,30 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
420
463
|
v._userFormats,
|
|
421
464
|
);
|
|
422
465
|
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
423
|
-
|
|
466
|
+
return { v, jsFn, jsErrFn };
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const sharedDecls = [];
|
|
470
|
+
{
|
|
471
|
+
const seen = new Set();
|
|
472
|
+
for (const e of compiledEntries) {
|
|
473
|
+
if (!e) continue;
|
|
474
|
+
for (const fn of [e.jsFn, e.jsErrFn]) {
|
|
475
|
+
if (!fn || !fn._sharedHelpers) continue;
|
|
476
|
+
for (const h of fn._sharedHelpers) if (!seen.has(h)) { seen.add(h); sharedDecls.push(h); }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const sharedSet = new Set(sharedDecls);
|
|
481
|
+
|
|
482
|
+
const fns = compiledEntries.map((e) => {
|
|
483
|
+
if (!e) return 'null';
|
|
484
|
+
const { v, jsFn, jsErrFn } = e;
|
|
485
|
+
let errBody =
|
|
424
486
|
jsErrFn && jsErrFn._errSource
|
|
425
487
|
? jsErrFn._errSource
|
|
426
488
|
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
489
|
+
for (const h of sharedDecls) errBody = errBody.split(h).join('');
|
|
427
490
|
// Custom format closures: embedded, or bound to the bundle-level
|
|
428
491
|
// registry when opts.formats is 'inject'.
|
|
429
492
|
let preamble = '';
|
|
@@ -433,11 +496,14 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
433
496
|
if (f.exportsSetFormats) bundleInjects = true;
|
|
434
497
|
}
|
|
435
498
|
// Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
|
|
436
|
-
// bundle output is self-contained
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
499
|
+
// bundle output is self-contained, minus anything lifted to module scope.
|
|
500
|
+
const kept = jsFn._preambleParts
|
|
501
|
+
? jsFn._preambleParts.filter((part) => !sharedSet.has(part))
|
|
502
|
+
: null;
|
|
503
|
+
const preambleSrc = kept !== null
|
|
504
|
+
? (jsFn._preambleGuard || '') + (kept.length ? kept.join('\n ') + '\n ' : '')
|
|
505
|
+
: (jsFn._preambleSource || '');
|
|
506
|
+
if (preambleSrc) preamble = preamble ? `${preamble}\n${preambleSrc}` : preambleSrc;
|
|
441
507
|
if (opts && opts.verbose) {
|
|
442
508
|
// Embed the schema and a small resolver so errors carry parentSchema.
|
|
443
509
|
const schemaLit = JSON.stringify(v._schemaObj);
|
|
@@ -446,7 +512,8 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
446
512
|
return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
447
513
|
});
|
|
448
514
|
const arr = `[${fns.join(',')}]`;
|
|
449
|
-
const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : ''
|
|
515
|
+
const safeEmbed = (bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '') +
|
|
516
|
+
(sharedDecls.length ? sharedDecls.join('\n') + '\n' : '');
|
|
450
517
|
const registry = bundleInjects
|
|
451
518
|
? `var __formats=Object.create(null);\nfunction setFormats(map){for(var k in map)__formats[k]=map[k]}\n`
|
|
452
519
|
: '';
|
|
@@ -489,16 +556,43 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
489
556
|
// Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
|
|
490
557
|
// hybrid body or it references undefined names. Prepending keeps dedup honest:
|
|
491
558
|
// schemas with different branch sets no longer collide on body alone.
|
|
492
|
-
const hybrid = jsFn._preambleSource
|
|
493
|
-
? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
|
|
494
|
-
: jsFn._hybridSource;
|
|
495
559
|
return {
|
|
496
|
-
|
|
560
|
+
guard: jsFn._preambleGuard || '',
|
|
561
|
+
parts: jsFn._preambleParts || null,
|
|
562
|
+
preamble: jsFn._preambleSource || '',
|
|
563
|
+
hybridBody: jsFn._hybridSource,
|
|
497
564
|
err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
|
|
498
565
|
fmt: jsFn._formatClosures || null,
|
|
566
|
+
shared: [
|
|
567
|
+
...(jsFn._sharedHelpers || []),
|
|
568
|
+
...((jsErrFn && jsErrFn._sharedHelpers) || []),
|
|
569
|
+
],
|
|
499
570
|
};
|
|
500
571
|
});
|
|
501
572
|
|
|
573
|
+
// A helper hoisted by several schemas is the same text declaring the same
|
|
574
|
+
// name, so the bundle holds one copy at module scope rather than one per
|
|
575
|
+
// schema. The rest of each preamble stays with its schema: those names are
|
|
576
|
+
// per compilation and would collide.
|
|
577
|
+
const sharedDecls = [];
|
|
578
|
+
{
|
|
579
|
+
const seen = new Set();
|
|
580
|
+
for (const e of entries) {
|
|
581
|
+
if (!e) continue;
|
|
582
|
+
for (const h of e.shared) if (!seen.has(h)) { seen.add(h); sharedDecls.push(h); }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const sharedSet = new Set(sharedDecls);
|
|
586
|
+
for (const e of entries) {
|
|
587
|
+
if (!e) continue;
|
|
588
|
+
const kept = e.parts ? e.parts.filter((part) => !sharedSet.has(part)) : null;
|
|
589
|
+
const preamble = kept !== null
|
|
590
|
+
? e.guard + (kept.length ? kept.join('\n ') + '\n ' : '')
|
|
591
|
+
: e.preamble;
|
|
592
|
+
e.hybrid = preamble ? `${preamble}\n${e.hybridBody}` : e.hybridBody;
|
|
593
|
+
if (e.err) for (const h of sharedDecls) e.err = e.err.split(h).join('');
|
|
594
|
+
}
|
|
595
|
+
|
|
502
596
|
// Deduplicate function bodies — many schemas produce identical or near-identical code
|
|
503
597
|
const bodyMap = new Map(); // body → index
|
|
504
598
|
const bodies = [];
|
|
@@ -531,6 +625,7 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
531
625
|
? '// Auto-generated by ata-validator — do not edit\n'
|
|
532
626
|
: "'use strict';\n";
|
|
533
627
|
if (bundleUsesSafeRe) out += getSafeRegexEmbed() + '\n';
|
|
628
|
+
if (sharedDecls.length) out += sharedDecls.join('\n') + '\n';
|
|
534
629
|
const declKW = isEsm ? 'const' : 'var';
|
|
535
630
|
out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
|
|
536
631
|
|
package/lib/branch-collapse.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Branch collapse: when no branch of an anyOf or oneOf matches, listing every
|
|
4
|
+
// branch's errors tells a reader that nothing fit without telling them which
|
|
5
|
+
// branch they meant. This scores the branches, reports the one that came
|
|
6
|
+
// closest, and keeps its errors under `branchErrors` for the pretty renderer.
|
|
7
|
+
//
|
|
8
|
+
// There is one implementation, and it is `__ataCollapse` below. A standalone
|
|
9
|
+
// module imports nothing, so the generated code cannot require this file; it
|
|
10
|
+
// carries a copy emitted by `embedSource()` from these very functions instead
|
|
11
|
+
// of a second hand-written one. That is why the internals are written in the
|
|
12
|
+
// positional form the generated code calls, and why `__ataCollapse` may close
|
|
13
|
+
// over nothing but `__ataScore` and the severity table: whatever it reaches
|
|
14
|
+
// has to be in the embed too.
|
|
15
|
+
|
|
16
|
+
// How much a failing keyword says about intent. A branch that fails on `type`
|
|
17
|
+
// was probably not the branch the author meant; one that fails on `minLength`
|
|
18
|
+
// probably was, and is the better thing to show. The error count dominates, so
|
|
19
|
+
// a branch with one complaint always beats a branch with three.
|
|
20
|
+
const __ATA_SEVERITY = {
|
|
4
21
|
type: 10,
|
|
5
22
|
const: 8,
|
|
6
23
|
enum: 8,
|
|
@@ -15,61 +32,69 @@ const SEVERITY = {
|
|
|
15
32
|
unevaluatedProperties: 2,
|
|
16
33
|
unevaluatedItems: 2,
|
|
17
34
|
};
|
|
18
|
-
const DEFAULT_SEVERITY = 4;
|
|
19
35
|
|
|
20
|
-
function
|
|
21
|
-
if (!
|
|
22
|
-
let
|
|
23
|
-
for (const e of
|
|
24
|
-
return
|
|
36
|
+
function __ataScore (errs) {
|
|
37
|
+
if (!errs || !errs.length) return 0;
|
|
38
|
+
let s = 0;
|
|
39
|
+
for (const e of errs) s += __ATA_SEVERITY[e.keyword] || 4;
|
|
40
|
+
return errs.length * 100 + s; // primary: count, secondary: severity
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
function collapseBranches ({ keyword, branchResults, parentPath, parentSchemaPath }) {
|
|
38
|
-
const passing = branchResults.filter(b => b.valid);
|
|
39
|
-
if (keyword === 'oneOf') {
|
|
40
|
-
if (passing.length === 1) return null;
|
|
41
|
-
if (passing.length > 1) {
|
|
43
|
+
// `br` is one entry per branch in declaration order: {valid, errors, title}.
|
|
44
|
+
// Returns the single error to report, or null when the keyword is satisfied.
|
|
45
|
+
function __ataCollapse (kw, br, pp, sp, o) {
|
|
46
|
+
const pass = br.filter((b) => b.valid);
|
|
47
|
+
if (kw === 'oneOf') {
|
|
48
|
+
if (pass.length === 1) return null;
|
|
49
|
+
if (pass.length > 1) {
|
|
50
|
+
const passIdx = [];
|
|
51
|
+
for (let i = 0; i < br.length; i++) if (br[i].valid) passIdx.push(i);
|
|
42
52
|
return {
|
|
43
|
-
code: 'ATA4002',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
code: 'ATA4002',
|
|
54
|
+
keyword: 'oneOf',
|
|
55
|
+
instancePath: pp || '',
|
|
56
|
+
path: pp || '',
|
|
57
|
+
schemaPath: sp,
|
|
58
|
+
_o: o,
|
|
59
|
+
message: 'value matched ' + pass.length + ' of ' + br.length + ' oneOf variants, expected exactly one',
|
|
60
|
+
params: { passingSchemas: passIdx },
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
|
-
|
|
50
|
-
return
|
|
63
|
+
} else if (pass.length >= 1) {
|
|
64
|
+
return null;
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
function buildBranchError (code, keyword, branchResults, parentPath, parentSchemaPath) {
|
|
58
|
-
let bestIdx = 0;
|
|
59
|
-
let bestScore = Infinity;
|
|
60
|
-
for (let i = 0; i < branchResults.length; i++) {
|
|
61
|
-
const s = scoreBranch(branchResults[i].errors);
|
|
62
|
-
if (s < bestScore) { bestScore = s; bestIdx = i; }
|
|
66
|
+
let bi = 0;
|
|
67
|
+
let bs = Infinity;
|
|
68
|
+
for (let i = 0; i < br.length; i++) {
|
|
69
|
+
const s = __ataScore(br[i].errors);
|
|
70
|
+
if (s < bs) { bs = s; bi = i; }
|
|
63
71
|
}
|
|
64
|
-
const best =
|
|
65
|
-
const variantName = best.title || `variant ${bestIdx + 1}`;
|
|
72
|
+
const best = br[bi];
|
|
66
73
|
return {
|
|
67
|
-
code
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
code: kw === 'oneOf' ? 'ATA4001' : 'ATA4003',
|
|
75
|
+
keyword: kw,
|
|
76
|
+
instancePath: pp || '',
|
|
77
|
+
path: pp || '',
|
|
78
|
+
schemaPath: sp,
|
|
79
|
+
_o: o,
|
|
80
|
+
message: 'value matched 0 of ' + br.length + ' ' + kw + ' variants',
|
|
81
|
+
params: { variants: br.length, closest: bi, closestName: best.title || ('variant ' + (bi + 1)) },
|
|
82
|
+
branchErrors: best.errors,
|
|
72
83
|
};
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
|
|
86
|
+
// The named-argument form the interpreter and the tests call.
|
|
87
|
+
function collapseBranches ({ keyword, branchResults, parentPath, parentSchemaPath, ordinal }) {
|
|
88
|
+
return __ataCollapse(keyword, branchResults, parentPath, parentSchemaPath, ordinal === undefined ? null : ordinal);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The three declarations as source, for the generated code to carry. Derived
|
|
92
|
+
// from the functions above rather than written out again, so a fix to one
|
|
93
|
+
// cannot miss the other. `tests/test_branch_collapse.js` runs both.
|
|
94
|
+
function embedSource () {
|
|
95
|
+
return 'const __ATA_SEVERITY=' + JSON.stringify(__ATA_SEVERITY) + ';' +
|
|
96
|
+
'const __ataScore=' + __ataScore.toString() + ';' +
|
|
97
|
+
'const __ataCollapse=' + __ataCollapse.toString() + ';';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { collapseBranches, scoreBranch: __ataScore, SEVERITY: __ATA_SEVERITY, embedSource };
|
package/lib/interpreter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { collapseBranches } = require('./branch-collapse');
|
|
2
3
|
|
|
3
4
|
// Interpreted draft 2020-12 engine. This is the correctness fallback for
|
|
4
5
|
// schemas the JS codegen cannot represent when the native engine is absent
|
|
@@ -866,7 +867,7 @@ class Interpreter {
|
|
|
866
867
|
valid = false;
|
|
867
868
|
} else {
|
|
868
869
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
869
|
-
if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath
|
|
870
|
+
if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath, stack, sub)) valid = false;
|
|
870
871
|
else if (collect) mergeAnnotations(local, sub);
|
|
871
872
|
}
|
|
872
873
|
}
|
|
@@ -905,7 +906,7 @@ class Interpreter {
|
|
|
905
906
|
valid = false;
|
|
906
907
|
} else {
|
|
907
908
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
908
|
-
if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath
|
|
909
|
+
if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath, stack, sub)) valid = false;
|
|
909
910
|
else if (collect) mergeAnnotations(local, sub);
|
|
910
911
|
}
|
|
911
912
|
}
|
|
@@ -985,7 +986,7 @@ class Interpreter {
|
|
|
985
986
|
}
|
|
986
987
|
const minC = P.minContains !== undefined ? P.minContains : 1;
|
|
987
988
|
if (matched.length < minC) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
|
|
988
|
-
if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('
|
|
989
|
+
if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC, maxContains: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
|
|
989
990
|
if (collect && matched.length) {
|
|
990
991
|
if (!local.items) local.items = new Set();
|
|
991
992
|
for (const i of matched) local.items.add(i);
|
|
@@ -1016,7 +1017,7 @@ class Interpreter {
|
|
|
1016
1017
|
}
|
|
1017
1018
|
if (P.propertyNames !== undefined) {
|
|
1018
1019
|
for (const key of keys) {
|
|
1019
|
-
if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath
|
|
1020
|
+
if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath, schemaPath + '/propertyNames', stack, DISCARD)) valid = false;
|
|
1020
1021
|
}
|
|
1021
1022
|
}
|
|
1022
1023
|
const props = P.properties;
|
|
@@ -1039,7 +1040,13 @@ class Interpreter {
|
|
|
1039
1040
|
}
|
|
1040
1041
|
}
|
|
1041
1042
|
if (!evaluated && P.additionalProperties !== undefined) {
|
|
1042
|
-
|
|
1043
|
+
// `additionalProperties: false` keeps its own keyword and names the
|
|
1044
|
+
// property it turned away. Running it as a plain false subschema
|
|
1045
|
+
// reported `not` at the key's path, which drops both.
|
|
1046
|
+
if (P.additionalProperties === false) {
|
|
1047
|
+
valid = false;
|
|
1048
|
+
if (errors !== NOERRORS) errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
|
|
1049
|
+
} else if (!this.eval(P.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, DISCARD)) valid = false;
|
|
1043
1050
|
evaluated = true;
|
|
1044
1051
|
}
|
|
1045
1052
|
if (evaluated && collect) {
|
|
@@ -1098,16 +1105,24 @@ class Interpreter {
|
|
|
1098
1105
|
if (P.anyOf !== null) {
|
|
1099
1106
|
let any = false;
|
|
1100
1107
|
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1108
|
+
// Per-branch errors, taken as a slice of the shared scratch rather than
|
|
1109
|
+
// an array each, so a branch that passes costs nothing extra.
|
|
1110
|
+
const branches = errors === NOERRORS ? null : [];
|
|
1101
1111
|
for (let i = 0; i < P.anyOf.length; i++) {
|
|
1102
1112
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1103
|
-
|
|
1113
|
+
const mark = branches === null ? 0 : scratch.length;
|
|
1114
|
+
const ok = this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub);
|
|
1115
|
+
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.anyOf[i]) });
|
|
1116
|
+
if (ok) {
|
|
1104
1117
|
any = true;
|
|
1105
1118
|
if (collect) mergeAnnotations(local, sub);
|
|
1106
1119
|
}
|
|
1107
1120
|
}
|
|
1108
1121
|
if (!any) {
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1122
|
+
if (branches !== null) {
|
|
1123
|
+
const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
|
|
1124
|
+
if (collapsed !== null) errors.push(collapsed);
|
|
1125
|
+
}
|
|
1111
1126
|
valid = false;
|
|
1112
1127
|
}
|
|
1113
1128
|
}
|
|
@@ -1115,17 +1130,25 @@ class Interpreter {
|
|
|
1115
1130
|
let count = 0;
|
|
1116
1131
|
let winner = null;
|
|
1117
1132
|
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1133
|
+
// Per-branch results, so a failing oneOf collapses to the branch that
|
|
1134
|
+
// came closest instead of every branch's complaints at once.
|
|
1135
|
+
const branches = errors === NOERRORS ? null : [];
|
|
1118
1136
|
for (let i = 0; i < P.oneOf.length; i++) {
|
|
1119
1137
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1120
|
-
|
|
1138
|
+
const mark = branches === null ? 0 : scratch.length;
|
|
1139
|
+
const ok = this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub);
|
|
1140
|
+
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.oneOf[i]) });
|
|
1141
|
+
if (ok) {
|
|
1121
1142
|
count++;
|
|
1122
1143
|
winner = sub;
|
|
1123
1144
|
}
|
|
1124
1145
|
}
|
|
1125
1146
|
if (count === 1) { if (collect) mergeAnnotations(local, winner); }
|
|
1126
1147
|
else {
|
|
1127
|
-
if (
|
|
1128
|
-
|
|
1148
|
+
if (branches !== null) {
|
|
1149
|
+
const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
|
|
1150
|
+
if (collapsed !== null) errors.push(collapsed);
|
|
1151
|
+
}
|
|
1129
1152
|
valid = false;
|
|
1130
1153
|
}
|
|
1131
1154
|
}
|
|
@@ -1182,6 +1205,12 @@ class Interpreter {
|
|
|
1182
1205
|
}
|
|
1183
1206
|
}
|
|
1184
1207
|
|
|
1208
|
+
// One branch title, for the collapsed anyOf/oneOf error. The compiled node
|
|
1209
|
+
// keeps the schema it came from, which is where a `title` lives.
|
|
1210
|
+
function branchTitle (node) {
|
|
1211
|
+
return node && node.schema && typeof node.schema.title === 'string' ? node.schema.title : '';
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1185
1214
|
function escapePointer(s) {
|
|
1186
1215
|
if (typeof s !== 'string') s = String(s);
|
|
1187
1216
|
// Keys almost never contain `~` or `/`; skip the two regex passes when so.
|
package/lib/js-compiler.js
CHANGED
|
@@ -25,6 +25,12 @@ const UQ_HELPERS = "function _cn(x){if(x===null||typeof x!=='object')return type
|
|
|
25
25
|
function hoistOnce(ctx, key, code) {
|
|
26
26
|
if (ctx[key]) return
|
|
27
27
|
ctx[key] = true
|
|
28
|
+
// Recorded as shared: the text is a constant and the name it declares is
|
|
29
|
+
// fixed, so several compiled schemas in one bundle can use one copy. The
|
|
30
|
+
// rest of the preamble cannot move, because emitConstant and the generated
|
|
31
|
+
// branch checks name things per compilation (_kv0, _av3) and two schemas
|
|
32
|
+
// would collide.
|
|
33
|
+
if (ctx.shared) ctx.shared.push(code)
|
|
28
34
|
if (ctx.preamble) ctx.preamble.push(code)
|
|
29
35
|
else if (ctx.helperCode) ctx.helperCode.push(code)
|
|
30
36
|
}
|
|
@@ -34,6 +40,18 @@ function emitDeq(ctx) {
|
|
|
34
40
|
return '_deq'
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
// RFC 6901: a property key reaching an instancePath has to have `~` and `/`
|
|
44
|
+
// escaped, or the pointer splits into the wrong segments. Static keys are
|
|
45
|
+
// escaped at compile time with ptrSeg; a key only known at run time needs
|
|
46
|
+
// this. It mirrors interpreter.js escapePointer, including skipping the two
|
|
47
|
+
// regex passes for the keys that need nothing, which is almost all of them.
|
|
48
|
+
const PTR_ESC_HELPER = "function _pe(s){for(let i=0;i<s.length;i++){const c=s.charCodeAt(i);if(c===126||c===47)return s.replace(/~/g,'~0').replace(/\\//g,'~1')}return s}"
|
|
49
|
+
|
|
50
|
+
function emitPtrEsc(ctx) {
|
|
51
|
+
hoistOnce(ctx, '_peHoisted', PTR_ESC_HELPER)
|
|
52
|
+
return '_pe'
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
// `_uq` compares items with `_deq`, so it can never be hoisted alone.
|
|
38
56
|
function emitUq(ctx) {
|
|
39
57
|
emitDeq(ctx)
|
|
@@ -1474,7 +1492,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1474
1492
|
if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
|
|
1475
1493
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
1476
1494
|
|
|
1477
|
-
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
|
|
1495
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
|
|
1478
1496
|
const lines = []
|
|
1479
1497
|
genCode(schema, 'd', lines, ctx)
|
|
1480
1498
|
|
|
@@ -1556,6 +1574,9 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1556
1574
|
const helperStr = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
|
|
1557
1575
|
boolFn._source = helperStr + body
|
|
1558
1576
|
boolFn._preambleSource = preambleStr
|
|
1577
|
+
boolFn._preambleGuard = guardStr
|
|
1578
|
+
boolFn._preambleParts = ctx.preamble ? ctx.preamble.slice() : []
|
|
1579
|
+
boolFn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
|
|
1559
1580
|
boolFn._hybridSource = helperStr + hybridBody
|
|
1560
1581
|
boolFn._usesSafeRe = !!ctx.usesSafeRe
|
|
1561
1582
|
// Custom-format closure entries that the bundle output needs to recreate.
|
|
@@ -3244,7 +3265,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3244
3265
|
if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
|
|
3245
3266
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
3246
3267
|
|
|
3247
|
-
const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null,
|
|
3268
|
+
const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null,
|
|
3248
3269
|
// Custom format checkers referenced by the body, bound as
|
|
3249
3270
|
// closure parameters below the way the other entry points do.
|
|
3250
3271
|
closureVars: [], closureVals: [],
|
|
@@ -3264,7 +3285,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3264
3285
|
// AOT output small for schemas that don't compose. Standalone modules can't
|
|
3265
3286
|
// require('./branch-collapse'), so the helper body is embedded verbatim.
|
|
3266
3287
|
if (ctx.usesBranchCollapse) {
|
|
3267
|
-
|
|
3288
|
+
ctx.helperCode.push(require('./branch-collapse').embedSource())
|
|
3268
3289
|
}
|
|
3269
3290
|
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
3270
3291
|
|
|
@@ -3314,6 +3335,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3314
3335
|
fn = new Function('d', '_all', body)
|
|
3315
3336
|
}
|
|
3316
3337
|
fn._errSource = body
|
|
3338
|
+
fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
|
|
3317
3339
|
fn._usesSafeRe = !!ctx.usesSafeRe
|
|
3318
3340
|
if (cvars.length) {
|
|
3319
3341
|
fn._formatClosures = cvars.map((name, i) => {
|
|
@@ -3490,10 +3512,11 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3490
3512
|
}
|
|
3491
3513
|
})
|
|
3492
3514
|
const expected = types.join(', ')
|
|
3515
|
+
const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
|
|
3493
3516
|
{
|
|
3494
3517
|
const typeSp = `${schemaPrefix}/type`
|
|
3495
3518
|
const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
|
|
3496
|
-
lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type
|
|
3519
|
+
lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
3497
3520
|
}
|
|
3498
3521
|
}
|
|
3499
3522
|
|
|
@@ -3703,9 +3726,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3703
3726
|
// properties — always guard (error mode, data may not be an object or may be array)
|
|
3704
3727
|
if (schema.properties) {
|
|
3705
3728
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3706
|
-
const childPath = childPathExpr(pathExpr,
|
|
3729
|
+
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
3707
3730
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
3708
|
-
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
3731
|
+
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
3709
3732
|
lines.push(`}`)
|
|
3710
3733
|
}
|
|
3711
3734
|
}
|
|
@@ -3727,7 +3750,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3727
3750
|
const ri = ctx.regExpMap.get(pattern);
|
|
3728
3751
|
const ki = ctx.varCounter++
|
|
3729
3752
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
|
|
3730
|
-
const
|
|
3753
|
+
const _peFn = emitPtrEsc(ctx)
|
|
3754
|
+
const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
|
|
3731
3755
|
// The real pointer, so source maps can locate the failing keyword.
|
|
3732
3756
|
genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/patternProperties/' + pat.replace(/~/g, '~0').replace(/\//g, '~1'))
|
|
3733
3757
|
lines.push(`}}}`)
|
|
@@ -3763,7 +3787,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3763
3787
|
if (patTests.length) conds.push(`!(${patTests.join('||')})`)
|
|
3764
3788
|
const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
|
|
3765
3789
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
|
|
3766
|
-
const
|
|
3790
|
+
const _peFn = emitPtrEsc(ctx)
|
|
3791
|
+
const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
|
|
3767
3792
|
genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
|
|
3768
3793
|
lines.push(`}}}`)
|
|
3769
3794
|
}
|
|
@@ -3772,7 +3797,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3772
3797
|
if (schema.dependentSchemas) {
|
|
3773
3798
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
3774
3799
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
3775
|
-
genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
|
|
3800
|
+
genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
|
|
3776
3801
|
lines.push(`}`)
|
|
3777
3802
|
}
|
|
3778
3803
|
}
|
|
@@ -3781,7 +3806,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3781
3806
|
// interpreter does (plan-compiler: instancePath + '/' + key).
|
|
3782
3807
|
if (schema.propertyNames === false) {
|
|
3783
3808
|
const ki = ctx.varCounter++
|
|
3784
|
-
|
|
3809
|
+
// The subschema is applied to the property NAME, so the failure belongs to
|
|
3810
|
+
// the object that carries the name, not to `/<key>`, which is where the
|
|
3811
|
+
// value lives. Same placement as the object-valued propertyNames branch.
|
|
3812
|
+
const p = pathExpr || '""'
|
|
3785
3813
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
|
|
3786
3814
|
}
|
|
3787
3815
|
|
|
@@ -3853,9 +3881,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3853
3881
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
3854
3882
|
const maxC = schema.maxContains
|
|
3855
3883
|
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
3856
|
-
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{
|
|
3884
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
|
|
3857
3885
|
if (maxC !== undefined) {
|
|
3858
|
-
lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{
|
|
3886
|
+
lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
|
|
3859
3887
|
}
|
|
3860
3888
|
lines.push(`}`)
|
|
3861
3889
|
}
|
|
@@ -3991,7 +4019,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3991
4019
|
if (hasUnresolvableRef(schema, cRootDefs, cAnchors, schemaMap, new Set())) return null
|
|
3992
4020
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
3993
4021
|
|
|
3994
|
-
const ctx = { varCounter: 0, helperCode: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
4022
|
+
const ctx = { varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
3995
4023
|
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema, userFormats: userFormats || null }
|
|
3996
4024
|
const lines = []
|
|
3997
4025
|
genCodeC(schema, 'd', '', lines, ctx, '#')
|
|
@@ -4137,10 +4165,11 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4137
4165
|
}
|
|
4138
4166
|
})
|
|
4139
4167
|
const expected = types.join(', ')
|
|
4168
|
+
const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
|
|
4140
4169
|
// Type check: push error but continue — wrap remaining in type-success block
|
|
4141
4170
|
const typeOk = `_tok${ctx.varCounter++}`
|
|
4142
4171
|
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
4143
|
-
lines.push(`if(!${typeOk}){${fail('type', 'type', `{type
|
|
4172
|
+
lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${expected}'`)}}`)
|
|
4144
4173
|
// Subsequent optimized code runs inside if(typeOk){...}
|
|
4145
4174
|
if (types.length === 1) {
|
|
4146
4175
|
isObj = types[0] === 'object'
|
|
@@ -4335,19 +4364,19 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4335
4364
|
if (schema.properties) {
|
|
4336
4365
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
4337
4366
|
const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
4338
|
-
const childPath = childPathExpr(pathExpr,
|
|
4367
|
+
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
4339
4368
|
if (requiredSet.has(key) && isObj) {
|
|
4340
4369
|
lines.push(`if(${pv}!==undefined){`)
|
|
4341
|
-
genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
4370
|
+
genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4342
4371
|
lines.push(`}`)
|
|
4343
4372
|
} else if (isObj) {
|
|
4344
4373
|
const oi = ctx.varCounter++
|
|
4345
4374
|
lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
|
|
4346
|
-
genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
4375
|
+
genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4347
4376
|
lines.push(`}}`)
|
|
4348
4377
|
} else {
|
|
4349
4378
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
4350
|
-
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
4379
|
+
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4351
4380
|
lines.push(`}`)
|
|
4352
4381
|
}
|
|
4353
4382
|
}
|
|
@@ -4420,7 +4449,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4420
4449
|
// The subschema is generated in place so its own keywords report
|
|
4421
4450
|
// the real instance path and schema pointer.
|
|
4422
4451
|
lines.push(`if(${matchers[i].check}){_m${pi}=true;{const _ppv${pi}_${i}=${v}[${kVar}]`)
|
|
4423
|
-
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4452
|
+
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4424
4453
|
lines.push(`}}`)
|
|
4425
4454
|
}
|
|
4426
4455
|
lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
|
|
@@ -4452,7 +4481,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4452
4481
|
}
|
|
4453
4482
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
4454
4483
|
lines.push(`if(${matchers[i].check}){const _ppv${pi}_${i}=${v}[${kVar}]`)
|
|
4455
|
-
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4484
|
+
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4456
4485
|
lines.push(`}`)
|
|
4457
4486
|
}
|
|
4458
4487
|
lines.push(`}}`)
|
|
@@ -4463,14 +4492,14 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4463
4492
|
if (schema.dependentSchemas) {
|
|
4464
4493
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
4465
4494
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
4466
|
-
genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
|
|
4495
|
+
genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
|
|
4467
4496
|
lines.push(`}`)
|
|
4468
4497
|
}
|
|
4469
4498
|
}
|
|
4470
4499
|
|
|
4471
4500
|
if (schema.propertyNames === false) {
|
|
4472
4501
|
const ki = ctx.varCounter++
|
|
4473
|
-
const p = pathExpr
|
|
4502
|
+
const p = pathExpr || '""'
|
|
4474
4503
|
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'})}}`)
|
|
4475
4504
|
}
|
|
4476
4505
|
|
|
@@ -4533,8 +4562,8 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4533
4562
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
4534
4563
|
const maxC = schema.maxContains
|
|
4535
4564
|
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
4536
|
-
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{
|
|
4537
|
-
if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{
|
|
4565
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
|
|
4566
|
+
if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
|
|
4538
4567
|
lines.push(`}`)
|
|
4539
4568
|
}
|
|
4540
4569
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The standardised output format from draft 2019-09 and 2020-12.
|
|
4
|
+
//
|
|
5
|
+
// ata reports errors in its own shape, the one consumers and the compat entry
|
|
6
|
+
// expect. The specification also defines an output format of its own, with
|
|
7
|
+
// `keywordLocation`, `absoluteKeywordLocation` and `instanceLocation`, and the
|
|
8
|
+
// official test suite has a small set of tests for it under `output-tests/`.
|
|
9
|
+
// This produces that shape from a validation, so those tests can be run and so
|
|
10
|
+
// a caller that wants spec output can have it.
|
|
11
|
+
//
|
|
12
|
+
// `flag` and `basic` are implemented. `detailed` and `verbose` are not: they
|
|
13
|
+
// need the full applicator tree, including the subschemas that passed, which
|
|
14
|
+
// the engines do not keep. The v1 `list` format is not implemented either; it
|
|
15
|
+
// is still moving, and the suite's own v1 output schema does not yet define it.
|
|
16
|
+
//
|
|
17
|
+
// Annotations are collected only when validation succeeded. That is not a
|
|
18
|
+
// shortcut: a subschema that fails contributes no annotations, and the output
|
|
19
|
+
// is rooted at the root schema, so a failing root drops all of them.
|
|
20
|
+
|
|
21
|
+
// Keywords whose value is the annotation they produce.
|
|
22
|
+
const METADATA_KEYWORDS = [
|
|
23
|
+
'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly',
|
|
24
|
+
'examples', 'format', 'contentEncoding', 'contentMediaType', 'contentSchema',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function pointerSegment (s) {
|
|
28
|
+
return String(s).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The root $id, which absoluteKeywordLocation is resolved against.
|
|
32
|
+
function baseUri (schema) {
|
|
33
|
+
if (schema && typeof schema === 'object' && typeof schema.$id === 'string') {
|
|
34
|
+
return schema.$id.split('#')[0];
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unitFor (error, base) {
|
|
40
|
+
const keywordLocation = typeof error.schemaPath === 'string' && error.schemaPath.charAt(0) === '#'
|
|
41
|
+
? error.schemaPath.slice(1)
|
|
42
|
+
: (error.schemaPath || '');
|
|
43
|
+
const unit = {
|
|
44
|
+
valid: false,
|
|
45
|
+
keywordLocation,
|
|
46
|
+
instanceLocation: error.instancePath || '',
|
|
47
|
+
};
|
|
48
|
+
if (base) unit.absoluteKeywordLocation = base + '#' + keywordLocation;
|
|
49
|
+
if (typeof error.message === 'string') unit.error = error.message;
|
|
50
|
+
return unit;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Walk the schema beside the instance, collecting metadata annotations from
|
|
54
|
+
// the subschemas that applied. Only reached when the whole validation passed,
|
|
55
|
+
// so every applicator followed here is one that succeeded, except the
|
|
56
|
+
// alternatives of anyOf/oneOf/if, which are checked before being followed.
|
|
57
|
+
function collectAnnotations (schema, data, keywordLocation, instanceLocation, base, out, isValid, depth) {
|
|
58
|
+
if (schema === true || schema === false || schema === null || typeof schema !== 'object' || depth > 64) return;
|
|
59
|
+
|
|
60
|
+
for (const kw of METADATA_KEYWORDS) {
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(schema, kw)) {
|
|
62
|
+
const unit = {
|
|
63
|
+
valid: true,
|
|
64
|
+
keywordLocation: keywordLocation + '/' + kw,
|
|
65
|
+
instanceLocation,
|
|
66
|
+
annotation: schema[kw],
|
|
67
|
+
};
|
|
68
|
+
if (base) unit.absoluteKeywordLocation = base + '#' + keywordLocation + '/' + kw;
|
|
69
|
+
out.push(unit);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const down = (sub, kwSeg, value, instSeg) => collectAnnotations(
|
|
74
|
+
sub, value, keywordLocation + kwSeg, instanceLocation + (instSeg || ''), base, out, isValid, depth + 1,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (schema.properties && data !== null && typeof data === 'object' && !Array.isArray(data)) {
|
|
78
|
+
for (const key of Object.keys(schema.properties)) {
|
|
79
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
80
|
+
down(schema.properties[key], '/properties/' + pointerSegment(key), data[key], '/' + pointerSegment(key));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(schema.prefixItems) && Array.isArray(data)) {
|
|
85
|
+
for (let i = 0; i < schema.prefixItems.length && i < data.length; i++) {
|
|
86
|
+
down(schema.prefixItems[i], '/prefixItems/' + i, data[i], '/' + i);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (schema.items && typeof schema.items === 'object' && Array.isArray(data)) {
|
|
90
|
+
const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0;
|
|
91
|
+
for (let i = start; i < data.length; i++) down(schema.items, '/items', data[i], '/' + i);
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(schema.allOf)) {
|
|
94
|
+
for (let i = 0; i < schema.allOf.length; i++) down(schema.allOf[i], '/allOf/' + i, data, '');
|
|
95
|
+
}
|
|
96
|
+
for (const kw of ['anyOf', 'oneOf']) {
|
|
97
|
+
if (Array.isArray(schema[kw])) {
|
|
98
|
+
for (let i = 0; i < schema[kw].length; i++) {
|
|
99
|
+
if (isValid(schema[kw][i], data)) down(schema[kw][i], '/' + kw + '/' + i, data, '');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (schema.if !== undefined) {
|
|
104
|
+
const taken = isValid(schema.if, data);
|
|
105
|
+
if (taken && schema.then !== undefined) down(schema.then, '/then', data, '');
|
|
106
|
+
if (!taken && schema.else !== undefined) down(schema.else, '/else', data, '');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Produce spec output for `data` under `validator`.
|
|
111
|
+
// format: 'flag' | 'basic' (default 'basic')
|
|
112
|
+
function toOutput (validator, data, opts) {
|
|
113
|
+
const format = (opts && opts.format) || 'basic';
|
|
114
|
+
const result = validator.validate(data);
|
|
115
|
+
if (format === 'flag') return { valid: result.valid };
|
|
116
|
+
if (format !== 'basic') {
|
|
117
|
+
throw new Error(`toOutput: unsupported format "${format}". Supported: "flag", "basic".`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const schema = validator._schemaObj !== undefined ? validator._schemaObj : validator._rawSchema;
|
|
121
|
+
const base = baseUri(schema);
|
|
122
|
+
const out = { valid: result.valid };
|
|
123
|
+
|
|
124
|
+
if (!result.valid) {
|
|
125
|
+
const errors = [];
|
|
126
|
+
for (const e of result.errors) errors.push(unitFor(e, base));
|
|
127
|
+
out.errors = errors;
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const annotations = [];
|
|
132
|
+
const isValid = (sub, value) => {
|
|
133
|
+
try { return new validator.constructor(sub).isValidObject(value); } catch { return false; }
|
|
134
|
+
};
|
|
135
|
+
collectAnnotations(schema, data, '', '', base, annotations, isValid, 0);
|
|
136
|
+
if (annotations.length) out.annotations = annotations;
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { toOutput, METADATA_KEYWORDS };
|
package/lib/plan-compiler.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { collapseBranches } = require('./branch-collapse');
|
|
3
|
+
|
|
4
|
+
// One branch title, for the collapsed anyOf/oneOf error. The compiled node
|
|
5
|
+
// keeps the schema it came from, which is where a `title` lives.
|
|
6
|
+
const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string' ? n.schema.title : '');
|
|
2
7
|
|
|
3
8
|
// Compiles interpreter Plans into a tree of specialized closures. No source
|
|
4
9
|
// generation, no `new Function`: this is plain closure composition, so it
|
|
@@ -334,13 +339,13 @@ function install(deps) {
|
|
|
334
339
|
if (collect) {
|
|
335
340
|
cstep = (data, errors, instancePath, schemaPath, stack, rec) => {
|
|
336
341
|
const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
|
|
337
|
-
const ok = target.c(data, errors, instancePath, schemaPath
|
|
342
|
+
const ok = target.c(data, errors, instancePath, schemaPath, stack, rec);
|
|
338
343
|
if (!ok) undo(rec, pl, n0, xl);
|
|
339
344
|
return ok;
|
|
340
345
|
};
|
|
341
346
|
vstep = (data, stack, rec) => target.v(data, stack, rec);
|
|
342
347
|
} else {
|
|
343
|
-
cstep = (data, errors, instancePath, schemaPath, stack) => target.c(data, errors, instancePath, schemaPath
|
|
348
|
+
cstep = (data, errors, instancePath, schemaPath, stack) => target.c(data, errors, instancePath, schemaPath, stack);
|
|
344
349
|
vstep = target.v;
|
|
345
350
|
}
|
|
346
351
|
if (ctx.guard) {
|
|
@@ -447,7 +452,10 @@ function install(deps) {
|
|
|
447
452
|
ok = false;
|
|
448
453
|
}
|
|
449
454
|
if (maxC !== undefined && matched > maxC) {
|
|
450
|
-
|
|
455
|
+
// Reported as `contains` at `#/contains`, the way the generated code
|
|
456
|
+
// and the reference implementation both do it: maxContains bounds the
|
|
457
|
+
// same keyword rather than being one of its own.
|
|
458
|
+
if (errors !== NOERRORS) errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC, maxContains: maxC }, `must NOT contain more than ${maxC} valid item(s)`));
|
|
451
459
|
ok = false;
|
|
452
460
|
}
|
|
453
461
|
return ok;
|
|
@@ -493,6 +501,13 @@ function install(deps) {
|
|
|
493
501
|
? P.patternProperties.map((e) => ({ re: e.re, src: e.src, fn: child(e.node) }))
|
|
494
502
|
: null;
|
|
495
503
|
const apFn = P.additionalProperties !== undefined ? child(P.additionalProperties) : null;
|
|
504
|
+
// `additionalProperties: false` is a boolean subschema, but its error is
|
|
505
|
+
// not the generic "boolean schema is false": every consumer switches on
|
|
506
|
+
// keyword === 'additionalProperties' and reads params.additionalProperty
|
|
507
|
+
// to learn which key was rejected. Reporting it as `not` at the key's own
|
|
508
|
+
// path loses both, and that shape is what the generated code emits, so
|
|
509
|
+
// the two engines have to agree here.
|
|
510
|
+
const apFalse = P.additionalProperties === false;
|
|
496
511
|
const pnFn = P.propertyNames !== undefined ? child(P.propertyNames) : null;
|
|
497
512
|
steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
|
|
498
513
|
if (dataBits(data) !== T_OBJECT) return true;
|
|
@@ -500,7 +515,12 @@ function install(deps) {
|
|
|
500
515
|
const keys = collect ? keysOf(rec, data) : Object.keys(data);
|
|
501
516
|
if (pnFn !== null) {
|
|
502
517
|
for (const k of keys) {
|
|
503
|
-
|
|
518
|
+
// propertyNames applies its subschema to the property NAME, not to
|
|
519
|
+
// the value sitting at that key, so the failure is reported against
|
|
520
|
+
// the object that carries the name rather than against `/<key>`.
|
|
521
|
+
// The generated code and the compat shim both do it this way, and
|
|
522
|
+
// the shim has to because that is where the reference puts it.
|
|
523
|
+
if (!pnFn.c(k, errors, instancePath, schemaPath + '/propertyNames', stack)) {
|
|
504
524
|
ok = false;
|
|
505
525
|
if (errors === NOERRORS) return false;
|
|
506
526
|
}
|
|
@@ -530,7 +550,11 @@ function install(deps) {
|
|
|
530
550
|
}
|
|
531
551
|
}
|
|
532
552
|
if (!evaluated && apFn !== null) {
|
|
533
|
-
if (
|
|
553
|
+
if (apFalse) {
|
|
554
|
+
ok = false;
|
|
555
|
+
if (errors === NOERRORS) return false;
|
|
556
|
+
errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
|
|
557
|
+
} else if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
|
|
534
558
|
ok = false;
|
|
535
559
|
if (errors === NOERRORS) return false;
|
|
536
560
|
}
|
|
@@ -717,20 +741,31 @@ function install(deps) {
|
|
|
717
741
|
}
|
|
718
742
|
if (P.anyOf !== null) {
|
|
719
743
|
const fns = P.anyOf.map((v) => inPlace(v, base));
|
|
744
|
+
const titles = P.anyOf.map(branchTitle);
|
|
720
745
|
steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
|
|
721
746
|
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
722
747
|
let any = false;
|
|
748
|
+
// Per-branch errors, taken as slices of the shared scratch rather than
|
|
749
|
+
// an array each, so a branch that passes costs nothing extra.
|
|
750
|
+
const branches = errors === NOERRORS ? null : [];
|
|
723
751
|
for (let i = 0; i < fns.length; i++) {
|
|
752
|
+
const mark = branches === null ? 0 : scratch.length;
|
|
753
|
+
let ok;
|
|
724
754
|
if (collect) {
|
|
725
755
|
const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
|
|
726
|
-
|
|
756
|
+
ok = fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, rec);
|
|
757
|
+
if (ok) any = true;
|
|
727
758
|
else undo(rec, pl, n0, xl);
|
|
728
|
-
} else
|
|
759
|
+
} else {
|
|
760
|
+
ok = fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack);
|
|
761
|
+
if (ok) any = true;
|
|
762
|
+
}
|
|
763
|
+
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: titles[i] });
|
|
729
764
|
}
|
|
730
765
|
if (!any) {
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
errors.push(
|
|
766
|
+
if (branches !== null) {
|
|
767
|
+
const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
|
|
768
|
+
if (collapsed !== null) errors.push(collapsed);
|
|
734
769
|
}
|
|
735
770
|
return false;
|
|
736
771
|
}
|
|
@@ -756,25 +791,36 @@ function install(deps) {
|
|
|
756
791
|
}
|
|
757
792
|
if (P.oneOf !== null) {
|
|
758
793
|
const fns = P.oneOf.map((v) => inPlace(v, base));
|
|
794
|
+
const titles = P.oneOf.map(branchTitle);
|
|
759
795
|
steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
|
|
760
796
|
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
761
797
|
let count = 0;
|
|
798
|
+
// Per-branch results, so a failing oneOf collapses to the branch that
|
|
799
|
+
// came closest instead of every branch's complaints at once.
|
|
800
|
+
const branches = errors === NOERRORS ? null : [];
|
|
762
801
|
// With a record: the winner's annotations stay, every other branch's
|
|
763
802
|
// are undone, and a second winner undoes everything since the node
|
|
764
803
|
// fails and merges nothing.
|
|
765
804
|
const pl0 = collect ? pLen(rec) : 0, n00 = collect ? rec.n : 0, xl0 = collect ? xLen(rec) : 0;
|
|
766
805
|
for (let i = 0; i < fns.length; i++) {
|
|
806
|
+
const mark = branches === null ? 0 : scratch.length;
|
|
807
|
+
let ok;
|
|
767
808
|
if (collect) {
|
|
768
809
|
const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
|
|
769
|
-
|
|
810
|
+
ok = fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, rec);
|
|
811
|
+
if (ok) count++;
|
|
770
812
|
else undo(rec, pl, n0, xl);
|
|
771
|
-
} else
|
|
813
|
+
} else {
|
|
814
|
+
ok = fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack);
|
|
815
|
+
if (ok) count++;
|
|
816
|
+
}
|
|
817
|
+
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: titles[i] });
|
|
772
818
|
}
|
|
773
819
|
if (count !== 1) {
|
|
774
820
|
if (collect) undo(rec, pl0, n00, xl0);
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
errors.push(
|
|
821
|
+
if (branches !== null) {
|
|
822
|
+
const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
|
|
823
|
+
if (collapsed !== null) errors.push(collapsed);
|
|
778
824
|
}
|
|
779
825
|
return false;
|
|
780
826
|
}
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
|
|
52
52
|
"build": "cmake-js build --target ata",
|
|
53
53
|
"rebuild": "cmake-js rebuild --target ata",
|
|
54
|
-
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
54
|
+
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
55
55
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
56
56
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
57
57
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -122,13 +122,13 @@
|
|
|
122
122
|
"LICENSE"
|
|
123
123
|
],
|
|
124
124
|
"optionalDependencies": {
|
|
125
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
126
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
127
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
128
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
129
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
130
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
131
|
-
"@ata-validator/native-win32-x64": "1.
|
|
125
|
+
"@ata-validator/native-darwin-arm64": "1.20.0",
|
|
126
|
+
"@ata-validator/native-darwin-x64": "1.20.0",
|
|
127
|
+
"@ata-validator/native-linux-arm64-gnu": "1.20.0",
|
|
128
|
+
"@ata-validator/native-linux-arm64-musl": "1.20.0",
|
|
129
|
+
"@ata-validator/native-linux-x64-gnu": "1.20.0",
|
|
130
|
+
"@ata-validator/native-linux-x64-musl": "1.20.0",
|
|
131
|
+
"@ata-validator/native-win32-x64": "1.20.0"
|
|
132
132
|
},
|
|
133
133
|
"peerDependencies": {
|
|
134
134
|
"yaml": "^2.0.0"
|