ata-validator 1.52.0 → 1.54.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 +7 -0
- package/compat.js +325 -29
- package/lib/codegen-engine.js +2 -1
- package/lib/compat-errors.js +131 -17
- package/lib/dialect.js +36 -1
- package/lib/draft7.js +80 -61
- package/lib/enrich-site.js +29 -2
- package/lib/error-runtime.js +6 -2
- package/lib/formats-source.js +43 -66
- package/lib/formats.js +37 -231
- package/lib/interpreter-compat.js +144 -0
- package/lib/interpreter-core.js +204 -17
- package/lib/js-compiler-errors.js +52 -16
- package/lib/js-compiler.js +540 -66
- package/lib/keywords.js +24 -1
- package/lib/levenshtein.js +21 -5
- package/lib/metaschemas.js +5 -3
- package/lib/plan-compiler.js +28 -6
- package/lib/regex-linear.js +81 -1
- package/lib/safe-regex.js +14 -11
- package/lib/schema-scan.js +20 -1
- package/lib/tier0.js +1 -0
- package/lib/unique-items.js +16 -5
- package/lib/uri-core.js +155 -0
- package/lib/validator-core.js +118 -23
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/interpreter-core.js
CHANGED
|
@@ -430,6 +430,11 @@ class Plan {
|
|
|
430
430
|
this.formatFn = fc || null;
|
|
431
431
|
}
|
|
432
432
|
this.hasString = this.minLength !== undefined || this.maxLength !== undefined || this.pattern !== null || this.formatFn !== null;
|
|
433
|
+
// A format compat registered for numbers (`type: 'number'`), checked on
|
|
434
|
+
// numbers as the reference checks it.
|
|
435
|
+
const nf = interp.numberFormats;
|
|
436
|
+
this.numFormatFn = schema.format !== undefined && nf !== null && typeof nf[schema.format] === 'function' ? nf[schema.format] : null;
|
|
437
|
+
if (this.numFormatFn !== null) this.hasNumber = true;
|
|
433
438
|
|
|
434
439
|
this.minItems = schema.minItems;
|
|
435
440
|
this.maxItems = schema.maxItems;
|
|
@@ -455,6 +460,21 @@ class Plan {
|
|
|
455
460
|
this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
|
|
456
461
|
}
|
|
457
462
|
}
|
|
463
|
+
// A required key holding undefined is missing. Where the key is also a
|
|
464
|
+
// declared property, the property walk, which reads the value anyway,
|
|
465
|
+
// finds that (requiredInProps); the required check reads the value only
|
|
466
|
+
// for the other keys (requiredUndef), so a valid object pays no second
|
|
467
|
+
// read of each required value.
|
|
468
|
+
this.requiredInProps = null;
|
|
469
|
+
this.requiredUndef = null;
|
|
470
|
+
if (this.required !== null) {
|
|
471
|
+
const undef = this.required.map((k) => this.properties === null || !this.properties.has(k));
|
|
472
|
+
if (undef.includes(true)) this.requiredUndef = undef;
|
|
473
|
+
if (this.properties !== null) {
|
|
474
|
+
const inProps = this.required.filter((k) => this.properties.has(k));
|
|
475
|
+
if (inProps.length > 0) this.requiredInProps = new Set(inProps);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
458
478
|
this.patternProperties = null;
|
|
459
479
|
if (isObjectMap(schema.patternProperties)) {
|
|
460
480
|
this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
|
|
@@ -521,16 +541,20 @@ class Plan {
|
|
|
521
541
|
if (typeof fn !== 'function') throw new Error(`keyword "${key}": compile must return a function`);
|
|
522
542
|
} else {
|
|
523
543
|
const validate = def.validate;
|
|
524
|
-
fn = (data) => validate(value, data, schema);
|
|
544
|
+
fn = def.dataContext ? (data, ctx) => validate(value, data, schema, ctx) : (data) => validate(value, data, schema);
|
|
525
545
|
// Errors written by the validate function land on the function
|
|
526
546
|
// itself, which is where the check reads them from below.
|
|
527
547
|
fn.source = validate;
|
|
528
548
|
}
|
|
529
549
|
if (this.custom === null) this.custom = [];
|
|
530
|
-
this.custom.push({ keyword: key, fn, mask });
|
|
550
|
+
this.custom.push({ keyword: key, fn, mask, ctx: def.dataContext === true, before: def.before || null, post: def.post === true });
|
|
531
551
|
}
|
|
552
|
+
if (this.custom !== null && this.custom.length > 1) this.custom = orderCustom(this.custom);
|
|
532
553
|
}
|
|
533
554
|
this.hasCustom = this.custom !== null;
|
|
555
|
+
// Keywords declared `post` run after everything else at the node, the
|
|
556
|
+
// subschemas included (see eval).
|
|
557
|
+
this.hasPost = this.custom !== null && this.custom.some((op) => op.post);
|
|
534
558
|
|
|
535
559
|
// Resource bookkeeping, read once instead of two Map lookups per visit.
|
|
536
560
|
// Only a resource root changes the base: a node inside a resource is
|
|
@@ -552,7 +576,10 @@ class Plan {
|
|
|
552
576
|
this.additionalProperties === undefined && this.propertyNames === undefined &&
|
|
553
577
|
this.dependentSchemas === null && this.propertyDependencies === null &&
|
|
554
578
|
this.prefixItems === null && this.items === undefined && this.contains === undefined &&
|
|
555
|
-
this.nodeBase === null
|
|
579
|
+
this.nodeBase === null &&
|
|
580
|
+
// The verdict-only leaf check carries no path, which a keyword reading
|
|
581
|
+
// its data's context needs.
|
|
582
|
+
!(this.custom !== null && this.custom.some((op) => op.ctx));
|
|
556
583
|
}
|
|
557
584
|
}
|
|
558
585
|
|
|
@@ -572,6 +599,7 @@ function evalLeafV(P, data) {
|
|
|
572
599
|
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
|
|
573
600
|
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) return false;
|
|
574
601
|
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) return false;
|
|
602
|
+
if (P.numFormatFn !== null && !P.numFormatFn(data)) return false;
|
|
575
603
|
}
|
|
576
604
|
if (P.hasString && bits === T_STRING) {
|
|
577
605
|
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) return false;
|
|
@@ -588,7 +616,8 @@ function evalLeafV(P, data) {
|
|
|
588
616
|
if (P.required !== null) {
|
|
589
617
|
const req = P.required;
|
|
590
618
|
for (let i = 0; i < req.length; i++) {
|
|
591
|
-
|
|
619
|
+
// A key holding undefined is missing, as in the default validator.
|
|
620
|
+
if (!Object.hasOwn(data, req[i]) || (P.requiredUndef !== null && P.requiredUndef[i] && data[req[i]] === undefined)) return false;
|
|
592
621
|
}
|
|
593
622
|
}
|
|
594
623
|
if (P.minProperties !== undefined && Object.keys(data).length < P.minProperties) return false;
|
|
@@ -610,13 +639,72 @@ function evalLeafV(P, data) {
|
|
|
610
639
|
// taking the error objects the keyword's function left on itself when it
|
|
611
640
|
// wrote any (`fn.errors`, the same convention as the default validator's
|
|
612
641
|
// custom keywords) and a fixed message otherwise.
|
|
613
|
-
|
|
642
|
+
// Custom keywords of one node in the order they declare: schema order, then
|
|
643
|
+
// each one with `before` moved ahead of the keyword it names (when that is
|
|
644
|
+
// one of them), then those marked `post` after the rest. A keyword that
|
|
645
|
+
// rewrites its value (serializing a Date to a string, say) declares that it
|
|
646
|
+
// runs before the one that checks the result.
|
|
647
|
+
function orderCustom(ops) {
|
|
648
|
+
const out = ops.filter((op) => !op.post);
|
|
649
|
+
for (const op of ops.slice()) {
|
|
650
|
+
if (op.before === null) continue;
|
|
651
|
+
const at = out.indexOf(op);
|
|
652
|
+
const target = out.findIndex((o) => o.keyword === op.before);
|
|
653
|
+
if (at < 0 || target < 0 || at < target) continue;
|
|
654
|
+
out.splice(at, 1);
|
|
655
|
+
out.splice(target, 0, op);
|
|
656
|
+
}
|
|
657
|
+
for (const op of ops) if (op.post) out.push(op);
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// The interpreter whose validate() is running a context-reading keyword, and
|
|
662
|
+
// the context it is given: where the value sits, read from the root by path.
|
|
663
|
+
let ctxInterp = null;
|
|
664
|
+
function dataCtx(instancePath, schemaPath, keyword) {
|
|
665
|
+
const root = ctxInterp !== null ? ctxInterp._ctxRoot : undefined;
|
|
666
|
+
if (instancePath === '') return { instancePath, parentData: undefined, parentDataProperty: undefined, rootData: root, schemaPath: schemaPath + '/' + keyword };
|
|
667
|
+
const cut = instancePath.lastIndexOf('/');
|
|
668
|
+
let parent = root;
|
|
669
|
+
if (cut > 0) {
|
|
670
|
+
const segs = instancePath.slice(1, cut).split('/');
|
|
671
|
+
for (let i = 0; i < segs.length && parent !== null && typeof parent === 'object'; i++) parent = parent[segs[i].replace(/~1/g, '/').replace(/~0/g, '~')];
|
|
672
|
+
}
|
|
673
|
+
let key = instancePath.slice(cut + 1).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
674
|
+
if (Array.isArray(parent) && /^(0|[1-9][0-9]*)$/.test(key)) key = +key;
|
|
675
|
+
return { instancePath, parentData: parent, parentDataProperty: key, rootData: root, schemaPath: schemaPath + '/' + keyword };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// After a context-reading keyword, the value is read again from where it sits:
|
|
679
|
+
// the keyword may have replaced it (a date deserialized, a value serialized),
|
|
680
|
+
// and the keywords after it check what it left, as the default validator's
|
|
681
|
+
// do after a modifying keyword.
|
|
682
|
+
function reread(ctx, data) {
|
|
683
|
+
const p = ctx.parentData;
|
|
684
|
+
return p !== null && typeof p === 'object' && ctx.parentDataProperty !== undefined ? p[ctx.parentDataProperty] : data;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Every branch's result, kept on a collapsed anyOf or oneOf error when the
|
|
688
|
+
// schema has keywords that read their data's context. Such a keyword may have
|
|
689
|
+
// changed the data, so the errors of each branch cannot be had afterwards by
|
|
690
|
+
// validating it again; compat reads them from here (see compat-errors.js).
|
|
691
|
+
function keepBranches(error, branches) {
|
|
692
|
+
Object.defineProperty(error, '_branches', { value: branches });
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function runCustomV(P, data, bits, instancePath, schemaPath) {
|
|
614
696
|
const ops = P.custom;
|
|
615
697
|
for (let i = 0; i < ops.length; i++) {
|
|
616
698
|
const op = ops[i];
|
|
617
699
|
if ((bits & op.mask) === 0) continue;
|
|
618
700
|
const fn = op.fn;
|
|
619
|
-
|
|
701
|
+
let ok;
|
|
702
|
+
if (op.ctx) {
|
|
703
|
+
const ctx = dataCtx(instancePath || '', schemaPath || '#', op.keyword);
|
|
704
|
+
ok = fn(data, ctx);
|
|
705
|
+
const now = reread(ctx, data);
|
|
706
|
+
if (now !== data) { data = now; bits = dataBits(data); }
|
|
707
|
+
} else ok = fn(data);
|
|
620
708
|
const holder = fn.source || fn;
|
|
621
709
|
if (holder.errors) holder.errors = null;
|
|
622
710
|
if (!ok) return false;
|
|
@@ -624,14 +712,23 @@ function runCustomV(P, data, bits) {
|
|
|
624
712
|
return true;
|
|
625
713
|
}
|
|
626
714
|
|
|
627
|
-
|
|
715
|
+
// `post`: undefined runs every keyword, false those not declared post, true
|
|
716
|
+
// only those.
|
|
717
|
+
function runCustom(P, data, bits, errors, instancePath, schemaPath, post) {
|
|
628
718
|
const ops = P.custom;
|
|
629
719
|
let valid = true;
|
|
630
720
|
for (let i = 0; i < ops.length; i++) {
|
|
631
721
|
const op = ops[i];
|
|
632
722
|
if ((bits & op.mask) === 0) continue;
|
|
723
|
+
if (post !== undefined && op.post !== post) continue;
|
|
633
724
|
const fn = op.fn;
|
|
634
|
-
|
|
725
|
+
let ok;
|
|
726
|
+
if (op.ctx) {
|
|
727
|
+
const ctx = dataCtx(instancePath, schemaPath, op.keyword);
|
|
728
|
+
ok = fn(data, ctx);
|
|
729
|
+
const now = reread(ctx, data);
|
|
730
|
+
if (now !== data) { data = now; bits = dataBits(data); }
|
|
731
|
+
} else ok = fn(data);
|
|
635
732
|
const holder = fn.source || fn;
|
|
636
733
|
const own = holder.errors;
|
|
637
734
|
if (own) holder.errors = null;
|
|
@@ -642,8 +739,10 @@ function runCustom(P, data, bits, errors, instancePath, schemaPath) {
|
|
|
642
739
|
if (Array.isArray(own) && own.length > 0) {
|
|
643
740
|
for (let j = 0; j < own.length; j++) {
|
|
644
741
|
const e = own[j];
|
|
742
|
+
// A context-reading keyword writes the path it was given, as the
|
|
743
|
+
// default validator's keywords do; the others write one relative to it.
|
|
645
744
|
errors.push(err(op.keyword, e.keyword || op.keyword,
|
|
646
|
-
e.instancePath !== undefined ? instancePath + e.instancePath : instancePath,
|
|
745
|
+
e.instancePath !== undefined ? (op.ctx ? e.instancePath : instancePath + e.instancePath) : instancePath,
|
|
647
746
|
e.schemaPath !== undefined ? e.schemaPath : kwPath,
|
|
648
747
|
e.params !== undefined ? e.params : {},
|
|
649
748
|
e.message !== undefined ? e.message : `must pass "${op.keyword}" keyword validation`));
|
|
@@ -681,6 +780,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
|
681
780
|
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
|
|
682
781
|
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
|
|
683
782
|
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
|
|
783
|
+
if (P.numFormatFn !== null && !P.numFormatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
|
|
684
784
|
}
|
|
685
785
|
if (P.hasString && bits === T_STRING) {
|
|
686
786
|
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) { errors === NOERRORS || errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
|
|
@@ -706,7 +806,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
|
706
806
|
const req = P.required;
|
|
707
807
|
for (let i = 0; i < req.length; i++) {
|
|
708
808
|
const key = req[i];
|
|
709
|
-
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
809
|
+
if (!Object.hasOwn(data, key) || (P.requiredUndef !== null && P.requiredUndef[i] && data[key] === undefined)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
710
810
|
}
|
|
711
811
|
}
|
|
712
812
|
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
@@ -731,6 +831,7 @@ class Interpreter {
|
|
|
731
831
|
this.root = rootSchema;
|
|
732
832
|
this.state = indexSchemas(rootSchema, opts.schemaMap, opts.flat === true);
|
|
733
833
|
this.userFormats = opts.formats || null;
|
|
834
|
+
this.numberFormats = opts.numberFormats && Object.keys(opts.numberFormats).length > 0 ? opts.numberFormats : null;
|
|
734
835
|
// Custom keywords, already normalized by lib/keywords.js.
|
|
735
836
|
this.keywords = opts.keywords || null;
|
|
736
837
|
// 2020-12 requires bookending for `$dynamicRef`; v1 removes the
|
|
@@ -744,6 +845,27 @@ class Interpreter {
|
|
|
744
845
|
this._fast = undefined;
|
|
745
846
|
this._calls = 0;
|
|
746
847
|
this._tier = typeof opts.tier === 'number' ? opts.tier : TIER;
|
|
848
|
+
// A keyword that asks for its data's context (parentData and the rest)
|
|
849
|
+
// needs every value's path, which only the collecting walk builds: the
|
|
850
|
+
// verdict comes from that walk too, and the closure tree is not used.
|
|
851
|
+
this.dataContext = false;
|
|
852
|
+
if (this.keywords !== null) for (const k of Object.keys(this.keywords)) if (this.keywords[k].dataContext) { this.dataContext = true; break; }
|
|
853
|
+
// Type coercion where each `type` is checked, the reference's way, which
|
|
854
|
+
// reaches a value behind allOf, anyOf, oneOf and $ref (the pass that runs
|
|
855
|
+
// before validation reaches `properties` and `items` only). It writes the
|
|
856
|
+
// coerced value where the value sits, so it takes the same walk.
|
|
857
|
+
this.coerce = opts.coerce === 'array' ? 'array' : opts.coerce === true;
|
|
858
|
+
if (this.coerce !== false) this.dataContext = true;
|
|
859
|
+
// OpenAPI's discriminator, read the reference's way under its
|
|
860
|
+
// `discriminator` option: the tag picks the one oneOf branch to apply.
|
|
861
|
+
this.discriminator = opts.discriminator === true;
|
|
862
|
+
this._discr = null;
|
|
863
|
+
// Both come from compat, which installs the code for them (see
|
|
864
|
+
// lib/interpreter-compat.js), so a bundle without compat carries none of it.
|
|
865
|
+
this.ext = this.coerce !== false || this.discriminator ? compatExt() : null;
|
|
866
|
+
// The compiled closures know none of these.
|
|
867
|
+
if (this.dataContext || this.discriminator || this.numberFormats !== null) this._fast = null;
|
|
868
|
+
this._ctxRoot = undefined;
|
|
747
869
|
}
|
|
748
870
|
|
|
749
871
|
// The closure-tree compiler covers every schema the interpreter accepts;
|
|
@@ -848,6 +970,7 @@ class Interpreter {
|
|
|
848
970
|
|
|
849
971
|
// Verdict only: same walk as validate(), no error construction at all.
|
|
850
972
|
isValid(data) {
|
|
973
|
+
if (this.dataContext) return this.validate(data).valid;
|
|
851
974
|
const fast = this._fast !== undefined ? this._fast : this._tiered();
|
|
852
975
|
if (fast !== null) return fast.v(data, fast.cyclic ? [] : null);
|
|
853
976
|
const dynScope = [this.state.rootBase];
|
|
@@ -859,6 +982,14 @@ class Interpreter {
|
|
|
859
982
|
}
|
|
860
983
|
}
|
|
861
984
|
|
|
985
|
+
// The value at `instancePath` as it is now, after a subschema may have
|
|
986
|
+
// coerced it.
|
|
987
|
+
here(instancePath, data) {
|
|
988
|
+
if (instancePath === '') return this._ctxRoot;
|
|
989
|
+
const ctx = dataCtx(instancePath, '', '');
|
|
990
|
+
return ctx.parentData !== null && typeof ctx.parentData === 'object' ? ctx.parentData[ctx.parentDataProperty] : data;
|
|
991
|
+
}
|
|
992
|
+
|
|
862
993
|
validate(data) {
|
|
863
994
|
const fast = this._fast !== undefined ? this._fast : this._tiered();
|
|
864
995
|
if (fast !== null) {
|
|
@@ -869,13 +1000,20 @@ class Interpreter {
|
|
|
869
1000
|
let errors = [];
|
|
870
1001
|
const dynScope = [this.state.rootBase];
|
|
871
1002
|
let valid;
|
|
1003
|
+
// The root a context-reading keyword resolves its parent from; kept
|
|
1004
|
+
// across a nested validate() its own function might make.
|
|
1005
|
+
const outerRoot = this._ctxRoot;
|
|
1006
|
+
if (this.dataContext) { this._ctxRoot = data; ctxInterp = this; }
|
|
872
1007
|
try {
|
|
873
1008
|
// Nothing reads the root's annotations, so they are discarded from the start.
|
|
874
1009
|
valid = this.eval(this.rootNode, data, this.state.rootBase, dynScope, errors, '', '#', [], DISCARD);
|
|
875
1010
|
} catch (e) {
|
|
1011
|
+
if (this.dataContext) throw e;
|
|
876
1012
|
const deep = this._deep(e);
|
|
877
1013
|
errors = [];
|
|
878
1014
|
valid = deep.c(data, errors, '', '#', deep.cyclic ? [] : null);
|
|
1015
|
+
} finally {
|
|
1016
|
+
if (this.dataContext) this._ctxRoot = outerRoot;
|
|
879
1017
|
}
|
|
880
1018
|
return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
|
|
881
1019
|
}
|
|
@@ -885,14 +1023,17 @@ class Interpreter {
|
|
|
885
1023
|
// `stack` breaks infinite $ref recursion on identical (schema, data) pairs.
|
|
886
1024
|
eval(P, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
|
|
887
1025
|
// Paths are built only while errors are collected; a verdict walk never
|
|
888
|
-
// reads them, and concatenating two per child was most of its cost.
|
|
889
|
-
|
|
1026
|
+
// reads them, and concatenating two per child was most of its cost. A
|
|
1027
|
+
// keyword that reads its data's context needs them on every walk.
|
|
1028
|
+
const E = errors !== NOERRORS || this.dataContext;
|
|
890
1029
|
if (P === true) return true;
|
|
891
1030
|
if (P === false) {
|
|
892
1031
|
if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
893
1032
|
return false;
|
|
894
1033
|
}
|
|
895
|
-
|
|
1034
|
+
// The leaf check cannot coerce: it neither knows the value's place nor
|
|
1035
|
+
// hands a coerced value on.
|
|
1036
|
+
if (P.leaf && this.coerce === false) return evalLeaf(P, data, errors, instancePath, schemaPath);
|
|
896
1037
|
const schema = P.schema;
|
|
897
1038
|
|
|
898
1039
|
// Only a reference can lead evaluation back to a schema it is already
|
|
@@ -926,6 +1067,9 @@ class Interpreter {
|
|
|
926
1067
|
const collect = sink !== DISCARD || P.hasUnevaluated;
|
|
927
1068
|
const local = collect ? { props: null, items: null } : DISCARD;
|
|
928
1069
|
|
|
1070
|
+
// Coercion first, before anything reads the value, as the reference does it.
|
|
1071
|
+
if (this.coerce !== false && P.hasType && (dataBits(data) & P.typeMask) === 0) data = this.ext.coerceAt(this, P, data, instancePath);
|
|
1072
|
+
|
|
929
1073
|
// $ref / $dynamicRef: in-place applicators, annotations flow through
|
|
930
1074
|
if (P.ref !== null) {
|
|
931
1075
|
const { child, base: refBase } = this.resolveRefCached(P, base);
|
|
@@ -977,6 +1121,7 @@ class Interpreter {
|
|
|
977
1121
|
else if (collect) mergeAnnotations(local, sub);
|
|
978
1122
|
}
|
|
979
1123
|
}
|
|
1124
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
980
1125
|
|
|
981
1126
|
const bits = dataBits(data);
|
|
982
1127
|
|
|
@@ -1003,6 +1148,7 @@ class Interpreter {
|
|
|
1003
1148
|
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
|
|
1004
1149
|
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
|
|
1005
1150
|
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
|
|
1151
|
+
if (P.numFormatFn !== null && !P.numFormatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
|
|
1006
1152
|
}
|
|
1007
1153
|
|
|
1008
1154
|
// strings
|
|
@@ -1018,7 +1164,7 @@ class Interpreter {
|
|
|
1018
1164
|
// them, so on a node that was not a leaf this walk skipped them and
|
|
1019
1165
|
// accepted what they reject; the plan compiler, which answered every call
|
|
1020
1166
|
// until calls were tiered, hid it.
|
|
1021
|
-
if (P.hasCustom && !runCustom(P, data, bits, errors, instancePath, schemaPath)) valid = false;
|
|
1167
|
+
if (P.hasCustom && !runCustom(P, data, bits, errors, instancePath, schemaPath, P.hasPost ? false : undefined)) valid = false;
|
|
1022
1168
|
|
|
1023
1169
|
// arrays
|
|
1024
1170
|
if (P.hasArray && bits === T_ARRAY) {
|
|
@@ -1070,7 +1216,7 @@ class Interpreter {
|
|
|
1070
1216
|
const req = P.required;
|
|
1071
1217
|
for (let i = 0; i < req.length; i++) {
|
|
1072
1218
|
const key = req[i];
|
|
1073
|
-
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
1219
|
+
if (!Object.hasOwn(data, key) || (P.requiredUndef !== null && P.requiredUndef[i] && data[key] === undefined)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
1074
1220
|
}
|
|
1075
1221
|
}
|
|
1076
1222
|
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
@@ -1096,7 +1242,14 @@ class Interpreter {
|
|
|
1096
1242
|
let evaluated = false;
|
|
1097
1243
|
const prop = props !== null ? props.get(key) : undefined;
|
|
1098
1244
|
if (prop !== undefined) {
|
|
1099
|
-
|
|
1245
|
+
// A declared property whose value is undefined is not checked, as the
|
|
1246
|
+
// generated code does not check it: no JSON document carries one, and
|
|
1247
|
+
// serialized the key is not there. Required, it is missing (see
|
|
1248
|
+
// requiredInProps).
|
|
1249
|
+
const value = data[key];
|
|
1250
|
+
if (value !== undefined) {
|
|
1251
|
+
if (!this.eval(prop.node, value, base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
|
|
1252
|
+
} else if (P.requiredInProps !== null && P.requiredInProps.has(key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
1100
1253
|
evaluated = true;
|
|
1101
1254
|
}
|
|
1102
1255
|
if (patterns !== null) {
|
|
@@ -1158,6 +1311,7 @@ class Interpreter {
|
|
|
1158
1311
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1159
1312
|
if (!this.eval(P.allOf[i], data, base, dynScope, errors, instancePath, E ? schemaPath + '/allOf/' + i : '', stack, sub)) valid = false;
|
|
1160
1313
|
else if (collect) mergeAnnotations(local, sub);
|
|
1314
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1161
1315
|
}
|
|
1162
1316
|
}
|
|
1163
1317
|
if (P.macros !== null) {
|
|
@@ -1186,16 +1340,25 @@ class Interpreter {
|
|
|
1186
1340
|
any = true;
|
|
1187
1341
|
if (collect) mergeAnnotations(local, sub);
|
|
1188
1342
|
}
|
|
1343
|
+
// Coercing, the branches after the first that passes are not tried,
|
|
1344
|
+
// as the reference does not try them: each could coerce the value.
|
|
1345
|
+
if (this.coerce !== false) {
|
|
1346
|
+
data = this.here(instancePath, data);
|
|
1347
|
+
if (ok && !collect) break;
|
|
1348
|
+
}
|
|
1189
1349
|
}
|
|
1190
1350
|
if (!any) {
|
|
1191
1351
|
if (branches !== null) {
|
|
1192
1352
|
const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
|
|
1353
|
+
if (collapsed !== null && this.dataContext) keepBranches(collapsed, branches);
|
|
1193
1354
|
if (collapsed !== null) errors.push(collapsed);
|
|
1194
1355
|
}
|
|
1195
1356
|
valid = false;
|
|
1196
1357
|
}
|
|
1197
1358
|
}
|
|
1198
|
-
if (P.oneOf !== null) {
|
|
1359
|
+
if (P.oneOf !== null && this.discriminator && this.ext.isDiscriminated(schema)) {
|
|
1360
|
+
if (bits === T_OBJECT && !this.ext.discriminate(this, P, data, base, dynScope, errors, instancePath, schemaPath, stack, collect ? local : DISCARD)) valid = false;
|
|
1361
|
+
} else if (P.oneOf !== null) {
|
|
1199
1362
|
let count = 0;
|
|
1200
1363
|
let winner = null;
|
|
1201
1364
|
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
@@ -1206,6 +1369,7 @@ class Interpreter {
|
|
|
1206
1369
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1207
1370
|
const mark = branches === null ? 0 : scratch.length;
|
|
1208
1371
|
const ok = this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, E ? schemaPath + '/oneOf/' + i : '', stack, sub);
|
|
1372
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1209
1373
|
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.oneOf[i]) });
|
|
1210
1374
|
if (ok) {
|
|
1211
1375
|
count++;
|
|
@@ -1216,6 +1380,7 @@ class Interpreter {
|
|
|
1216
1380
|
else {
|
|
1217
1381
|
if (branches !== null) {
|
|
1218
1382
|
const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
|
|
1383
|
+
if (collapsed !== null && this.dataContext) keepBranches(collapsed, branches);
|
|
1219
1384
|
if (collapsed !== null) errors.push(collapsed);
|
|
1220
1385
|
}
|
|
1221
1386
|
valid = false;
|
|
@@ -1227,11 +1392,13 @@ class Interpreter {
|
|
|
1227
1392
|
if (errors !== NOERRORS) errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
|
|
1228
1393
|
valid = false;
|
|
1229
1394
|
}
|
|
1395
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1230
1396
|
}
|
|
1231
1397
|
if (P.if !== undefined) {
|
|
1232
1398
|
const ifSub = collect ? { props: null, items: null } : DISCARD;
|
|
1233
1399
|
const ifScratch = errors === NOERRORS ? NOERRORS : [];
|
|
1234
1400
|
const ifOk = this.eval(P.if, data, base, dynScope, ifScratch, instancePath, E ? schemaPath + '/if' : '', stack, ifSub);
|
|
1401
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1235
1402
|
if (ifOk) {
|
|
1236
1403
|
if (collect) mergeAnnotations(local, ifSub);
|
|
1237
1404
|
if (P.then !== undefined) {
|
|
@@ -1281,6 +1448,11 @@ class Interpreter {
|
|
|
1281
1448
|
}
|
|
1282
1449
|
}
|
|
1283
1450
|
|
|
1451
|
+
if (P.hasPost) {
|
|
1452
|
+
if (this.dataContext) data = this.here(instancePath, data);
|
|
1453
|
+
if (!runCustom(P, data, dataBits(data), errors, instancePath, schemaPath, true)) valid = false;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1284
1456
|
if (scopePushed) dynScope.pop();
|
|
1285
1457
|
if (tracked) stack.length -= 2;
|
|
1286
1458
|
if (valid && collect && sink !== DISCARD) mergeAnnotations(sink, local);
|
|
@@ -1336,12 +1508,27 @@ function planCompiler() {
|
|
|
1336
1508
|
return compileInterpreter;
|
|
1337
1509
|
}
|
|
1338
1510
|
|
|
1511
|
+
let installCompat = null;
|
|
1512
|
+
let compatInstalled = null;
|
|
1513
|
+
function setCompat(install) {
|
|
1514
|
+
installCompat = install;
|
|
1515
|
+
compatInstalled = null;
|
|
1516
|
+
}
|
|
1517
|
+
function compatExt() {
|
|
1518
|
+
if (compatInstalled === null) {
|
|
1519
|
+
if (installCompat === null) throw new Error('coercion where each type is checked and the discriminator are installed by ata-validator/compat');
|
|
1520
|
+
compatInstalled = installCompat({ NOERRORS, DISCARD, err, mergeAnnotations, dataCtx });
|
|
1521
|
+
}
|
|
1522
|
+
return compatInstalled;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1339
1525
|
function createInterpreter(schema, options) {
|
|
1340
1526
|
return new Interpreter(schema, options);
|
|
1341
1527
|
}
|
|
1342
1528
|
|
|
1343
1529
|
module.exports = { createInterpreter };
|
|
1344
1530
|
Object.defineProperty(module.exports, '_setPlanCompiler', { value: setPlanCompiler, enumerable: false });
|
|
1531
|
+
Object.defineProperty(module.exports, '_setCompat', { value: setCompat, enumerable: false });
|
|
1345
1532
|
// The resolver, for the code generator: it resolves references at compile
|
|
1346
1533
|
// time the way this engine does at run time (normalizeRefs in js-compiler).
|
|
1347
1534
|
Object.defineProperty(module.exports, '_refInternals', {
|
|
@@ -9,7 +9,7 @@ const { compileFunction } = require('./compile-fn')
|
|
|
9
9
|
// the first call that compiles errors. Everything here was written as part
|
|
10
10
|
// of js-compiler.js and reads its helpers from that module's _internal.
|
|
11
11
|
const core = require('./js-compiler')
|
|
12
|
-
const { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
|
|
12
|
+
const { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag } = core._internal
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
function emitPtrEsc(ctx) {
|
|
@@ -590,9 +590,19 @@ function subtreeGuard (sub, ctx, gv) {
|
|
|
590
590
|
// 25-definition API schema came to 55 KB of source in a 133 KB function,
|
|
591
591
|
// and a function that size reaches the optimizing compiler late.
|
|
592
592
|
ctx.shareDefs = true
|
|
593
|
+
// A record of many optional properties takes the verdict's key-loop form
|
|
594
|
+
// (keyLoopEligible): its optional properties are checked from the record's
|
|
595
|
+
// own keys. Written one read per property, the guard of a 150-property
|
|
596
|
+
// record was 16.9 KB, over GUARD_MAX, so there was no guard at all and
|
|
597
|
+
// every record of the list was collected in full; on records of mixed
|
|
598
|
+
// shapes those reads are megamorphic, and 2000 of them cost 18 ms where
|
|
599
|
+
// the verdict took 0.3. The key-loop form is 10 KB and reads only the keys
|
|
600
|
+
// the record has.
|
|
601
|
+
const outerKeyLoop = ctx.keyLoopNode
|
|
602
|
+
if (keyLoopEligible(sub)) ctx.keyLoopNode = sub
|
|
593
603
|
try {
|
|
594
604
|
const bl = []
|
|
595
|
-
try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare }
|
|
605
|
+
try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare; ctx.keyLoopNode = outerKeyLoop }
|
|
596
606
|
if (bl.length === 0) { restoreCtx(ctx, saved); return null }
|
|
597
607
|
const body = bl.join(';')
|
|
598
608
|
// The verdict generator's recursion names (`_validate`, the cycle guard
|
|
@@ -612,17 +622,20 @@ function subtreeGuard (sub, ctx, gv) {
|
|
|
612
622
|
}
|
|
613
623
|
}
|
|
614
624
|
// The collecting code for `sub` at `v`, behind its guard when there is one.
|
|
615
|
-
|
|
625
|
+
// `when`, a boolean variable, makes the guard conditional: when it is false
|
|
626
|
+
// the value is collected without a verdict first (see the items case).
|
|
627
|
+
function genGuardedC (sub, v, pathExpr, lines, ctx, schemaPrefix, when) {
|
|
616
628
|
const gi = ctx.varCounter++
|
|
617
629
|
const gv = `_gv${gi}`
|
|
618
630
|
const g = subtreeGuard(sub, ctx, gv)
|
|
619
631
|
if (g === null) { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix); return }
|
|
620
|
-
|
|
632
|
+
const cond = when ? `!${when}||` : ''
|
|
633
|
+
if (g.name !== undefined) lines.push(`if(${cond}!${g.name}(${v})){`)
|
|
621
634
|
else {
|
|
622
635
|
// The verdict's exits become jumps out of the block: a failure leaves
|
|
623
636
|
// the flag set, success clears it. replaceTopLevel keeps nested
|
|
624
637
|
// closures' returns as they are (see the hybrid rewrite).
|
|
625
|
-
lines.push(`{const ${gv}=${v};let _g${gi}=1
|
|
638
|
+
lines.push(`{const ${gv}=${v};let _g${gi}=1;${when ? `if(${when})` : ''}g${gi}:{${replaceTopLevel(g.body, `{_g${gi}=0;break g${gi}}`, `break g${gi}`)};_g${gi}=0}if(_g${gi}){`)
|
|
626
639
|
}
|
|
627
640
|
ctx.guardDepth = (ctx.guardDepth || 0) + 1
|
|
628
641
|
try { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix) } finally { ctx.guardDepth-- }
|
|
@@ -1164,7 +1177,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1164
1177
|
const reqSp = `${schemaPrefix}/required`
|
|
1165
1178
|
const reqLit = buildErrorLiteral({ keyword: 'required', schemaPath: reqSp, sourceMap: ctx.sourceMap })
|
|
1166
1179
|
for (const key of schema.required) {
|
|
1167
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})
|
|
1180
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
1168
1181
|
}
|
|
1169
1182
|
}
|
|
1170
1183
|
|
|
@@ -1354,7 +1367,8 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1354
1367
|
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
1355
1368
|
const sp = schemaPrefix+'/properties/'+ptrSeg(key)
|
|
1356
1369
|
const from = lines.length
|
|
1357
|
-
|
|
1370
|
+
// A value that is undefined is not checked (see the verdict generator).
|
|
1371
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${v}[${JSON.stringify(key)}]!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
|
|
1358
1372
|
// A large subschema as a function of its own (emitPropFnE); false means inline.
|
|
1359
1373
|
if (!propFnCandidateE(prop, ctx, sp) || !emitPropFnE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)) {
|
|
1360
1374
|
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)
|
|
@@ -1769,7 +1783,10 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
|
|
|
1769
1783
|
// the definition and the reference's own comes in as `_sp`.
|
|
1770
1784
|
const rel = sp.startsWith(SP_PLACEHOLDER)
|
|
1771
1785
|
ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
|
|
1772
|
-
|
|
1786
|
+
// The site's ordinal key, as the plain form passes it: a list that comes out
|
|
1787
|
+
// in order then needs no sorting, and one that does not is put in order from
|
|
1788
|
+
// the keys collected beside it (sr) instead of looking up each error's path.
|
|
1789
|
+
return `(_e=_ap(_A,_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}),${apOrd(ctx, sp)}))`
|
|
1773
1790
|
}
|
|
1774
1791
|
|
|
1775
1792
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
|
|
@@ -1935,7 +1952,10 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
1935
1952
|
// The checked sort (_srX) compares with the reader, which a module
|
|
1936
1953
|
// does not carry: there `_SRT` keeps the list as it is.
|
|
1937
1954
|
`\n return _e?new _ER(_A.mo?_e:${env('ATA_CHECK_SORT') && !(opts && opts.emit) ? '_srX(_A,_e,_SRT)' : '_sr(_A,_e,_SRT)'}):${opts && opts.moduleShape ? 'R' : '{valid:true,data:d,errors:_EE}'}}catch(_x){return _FB(d)}`
|
|
1938
|
-
|
|
1955
|
+
// The rich form also says whether its list came out in the reader's
|
|
1956
|
+
// order (`mo`, kept by _ap from each site's ordinal key), so the reader
|
|
1957
|
+
// (richBuilder) sorts only a list that did not.
|
|
1958
|
+
: hoistDecl + allDecl + `let _e;${AP_RESET}\n ` + checks + `\n return _e?{valid:false,errors:_e${ctx.rich ? ',mo:_A.mo' : ''}}:R`
|
|
1939
1959
|
// The verdict generator's hoisted functions (subtreeGuard writes guards
|
|
1940
1960
|
// through it) come first: a oneOf branch it could not hoist was a closure
|
|
1941
1961
|
// built at every call of the guard, six per element of a json-patch list.
|
|
@@ -1952,7 +1972,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
1952
1972
|
console.log('=== COMBINED PARTS ===', JSON.stringify(Object.entries(acc).sort((a, b) => b[1] - a[1])))
|
|
1953
1973
|
}
|
|
1954
1974
|
const programSrc = assignOrdKeys(ctx, `${helpers}return function _vC(d){${inner}}`)
|
|
1955
|
-
|
|
1975
|
+
// The run-time program is tagged (seqTag); the emitted source is not.
|
|
1976
|
+
const factory = compileFunction('R' + (closureParams ? ',' + closureParams : ''), (opts && opts.emit ? '' : seqTag()) + programSrc)
|
|
1956
1977
|
const made = factory(VALID_RESULT, ...closureArgs)
|
|
1957
1978
|
// For a standalone module (lib/aot-impl.js): the program's source and the
|
|
1958
1979
|
// closure values it takes, after the function was built and tried on
|
|
@@ -2405,7 +2426,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2405
2426
|
}
|
|
2406
2427
|
}
|
|
2407
2428
|
for (const key of schema.required) {
|
|
2408
|
-
const check = hoisted[key] ? `${hoisted[key]}===undefined` :
|
|
2429
|
+
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})`
|
|
2409
2430
|
if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
|
|
2410
2431
|
const ei = ctx.varCounter++
|
|
2411
2432
|
const errVar = `_E${ei}`
|
|
@@ -2428,7 +2449,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2428
2449
|
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
2429
2450
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_A,_e,${errVar},${apOrd(ctx, `${schemaPrefix}/required`)}))}`)
|
|
2430
2451
|
} else {
|
|
2431
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})
|
|
2452
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
2432
2453
|
}
|
|
2433
2454
|
}
|
|
2434
2455
|
}
|
|
@@ -2612,7 +2633,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2612
2633
|
sub(`_o${oi}`)
|
|
2613
2634
|
lines.push(`}}`)
|
|
2614
2635
|
} else {
|
|
2615
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
2636
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${v}[${JSON.stringify(key)}]!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
|
|
2616
2637
|
sub(`${v}[${JSON.stringify(key)}]`)
|
|
2617
2638
|
lines.push(`}`)
|
|
2618
2639
|
}
|
|
@@ -2643,9 +2664,24 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2643
2664
|
const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
|
|
2644
2665
|
ctx.varCounter++
|
|
2645
2666
|
const childPath = childPathDynExpr(pathExpr, idx)
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2667
|
+
if (ctx.inCombined && keyLoopEligible(schema.items)) {
|
|
2668
|
+
// Records of many optional properties (see subtreeGuard): the guard,
|
|
2669
|
+
// a key loop per record, is run only for a list of mixed shapes,
|
|
2670
|
+
// decided as the verdict decides (the first, middle and last record's
|
|
2671
|
+
// keys). On records of one shape the collecting code's reads are
|
|
2672
|
+
// monomorphic and cheaper than any guard: 2000 uniform records read
|
|
2673
|
+
// for errors in 16 us, with a guard on each 69.
|
|
2674
|
+
hoistOnce(ctx, 'sameKeysHelper', SAME_KEYS_HELPER)
|
|
2675
|
+
const n = `_n${ctx.varCounter++}`
|
|
2676
|
+
const mx = `_mx${ctx.varCounter++}`
|
|
2677
|
+
lines.push(`if(Array.isArray(${v})){const ${n}=${v}.length;const ${mx}=${n}-${startIdx}>2&&(!_sameKeys(${v}[${startIdx}],${v}[(${n}+${startIdx})>>1])||!_sameKeys(${v}[${startIdx}],${v}[${n}-1]));for(let ${idx}=${startIdx};${idx}<${n};${idx}++){const ${elem}=${v}[${idx}]`)
|
|
2678
|
+
genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items', mx)
|
|
2679
|
+
lines.push(`}}`)
|
|
2680
|
+
} else {
|
|
2681
|
+
lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
2682
|
+
genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
|
|
2683
|
+
lines.push(`}}`)
|
|
2684
|
+
}
|
|
2649
2685
|
}
|
|
2650
2686
|
|
|
2651
2687
|
// prefixItems
|