ata-validator 1.51.0 → 1.53.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 +26 -20
- package/compat.js +314 -29
- package/index.bundle.mjs +15 -0
- package/lib/aot-impl.js +2 -2
- package/lib/combined-runtime.js +4 -1
- package/lib/compat-errors.js +131 -17
- package/lib/draft7.js +65 -35
- package/lib/error-runtime.js +5 -1
- package/lib/interpreter-core.js +297 -14
- package/lib/js-compiler-errors.js +15 -15
- package/lib/js-compiler.js +196 -45
- package/lib/keywords.js +24 -1
- package/lib/levenshtein.js +21 -5
- package/lib/plan-compiler.js +8 -3
- package/lib/safe-regex.js +33 -14
- package/lib/schema-scan.js +29 -1
- package/lib/validator-core.js +99 -13
- package/lib/version.js +1 -1
- package/package.json +10 -8
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;
|
|
@@ -521,16 +526,20 @@ class Plan {
|
|
|
521
526
|
if (typeof fn !== 'function') throw new Error(`keyword "${key}": compile must return a function`);
|
|
522
527
|
} else {
|
|
523
528
|
const validate = def.validate;
|
|
524
|
-
fn = (data) => validate(value, data, schema);
|
|
529
|
+
fn = def.dataContext ? (data, ctx) => validate(value, data, schema, ctx) : (data) => validate(value, data, schema);
|
|
525
530
|
// Errors written by the validate function land on the function
|
|
526
531
|
// itself, which is where the check reads them from below.
|
|
527
532
|
fn.source = validate;
|
|
528
533
|
}
|
|
529
534
|
if (this.custom === null) this.custom = [];
|
|
530
|
-
this.custom.push({ keyword: key, fn, mask });
|
|
535
|
+
this.custom.push({ keyword: key, fn, mask, ctx: def.dataContext === true, before: def.before || null, post: def.post === true });
|
|
531
536
|
}
|
|
537
|
+
if (this.custom !== null && this.custom.length > 1) this.custom = orderCustom(this.custom);
|
|
532
538
|
}
|
|
533
539
|
this.hasCustom = this.custom !== null;
|
|
540
|
+
// Keywords declared `post` run after everything else at the node, the
|
|
541
|
+
// subschemas included (see eval).
|
|
542
|
+
this.hasPost = this.custom !== null && this.custom.some((op) => op.post);
|
|
534
543
|
|
|
535
544
|
// Resource bookkeeping, read once instead of two Map lookups per visit.
|
|
536
545
|
// Only a resource root changes the base: a node inside a resource is
|
|
@@ -552,7 +561,10 @@ class Plan {
|
|
|
552
561
|
this.additionalProperties === undefined && this.propertyNames === undefined &&
|
|
553
562
|
this.dependentSchemas === null && this.propertyDependencies === null &&
|
|
554
563
|
this.prefixItems === null && this.items === undefined && this.contains === undefined &&
|
|
555
|
-
this.nodeBase === null
|
|
564
|
+
this.nodeBase === null &&
|
|
565
|
+
// The verdict-only leaf check carries no path, which a keyword reading
|
|
566
|
+
// its data's context needs.
|
|
567
|
+
!(this.custom !== null && this.custom.some((op) => op.ctx));
|
|
556
568
|
}
|
|
557
569
|
}
|
|
558
570
|
|
|
@@ -572,6 +584,7 @@ function evalLeafV(P, data) {
|
|
|
572
584
|
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
|
|
573
585
|
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) return false;
|
|
574
586
|
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) return false;
|
|
587
|
+
if (P.numFormatFn !== null && !P.numFormatFn(data)) return false;
|
|
575
588
|
}
|
|
576
589
|
if (P.hasString && bits === T_STRING) {
|
|
577
590
|
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) return false;
|
|
@@ -610,13 +623,137 @@ function evalLeafV(P, data) {
|
|
|
610
623
|
// taking the error objects the keyword's function left on itself when it
|
|
611
624
|
// wrote any (`fn.errors`, the same convention as the default validator's
|
|
612
625
|
// custom keywords) and a fixed message otherwise.
|
|
613
|
-
|
|
626
|
+
// Custom keywords of one node in the order they declare: schema order, then
|
|
627
|
+
// each one with `before` moved ahead of the keyword it names (when that is
|
|
628
|
+
// one of them), then those marked `post` after the rest. A keyword that
|
|
629
|
+
// rewrites its value (serializing a Date to a string, say) declares that it
|
|
630
|
+
// runs before the one that checks the result.
|
|
631
|
+
function orderCustom(ops) {
|
|
632
|
+
const out = ops.filter((op) => !op.post);
|
|
633
|
+
for (const op of ops.slice()) {
|
|
634
|
+
if (op.before === null) continue;
|
|
635
|
+
const at = out.indexOf(op);
|
|
636
|
+
const target = out.findIndex((o) => o.keyword === op.before);
|
|
637
|
+
if (at < 0 || target < 0 || at < target) continue;
|
|
638
|
+
out.splice(at, 1);
|
|
639
|
+
out.splice(target, 0, op);
|
|
640
|
+
}
|
|
641
|
+
for (const op of ops) if (op.post) out.push(op);
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// The interpreter whose validate() is running a context-reading keyword, and
|
|
646
|
+
// the context it is given: where the value sits, read from the root by path.
|
|
647
|
+
let ctxInterp = null;
|
|
648
|
+
function dataCtx(instancePath, schemaPath, keyword) {
|
|
649
|
+
const root = ctxInterp !== null ? ctxInterp._ctxRoot : undefined;
|
|
650
|
+
if (instancePath === '') return { instancePath, parentData: undefined, parentDataProperty: undefined, rootData: root, schemaPath: schemaPath + '/' + keyword };
|
|
651
|
+
const cut = instancePath.lastIndexOf('/');
|
|
652
|
+
let parent = root;
|
|
653
|
+
if (cut > 0) {
|
|
654
|
+
const segs = instancePath.slice(1, cut).split('/');
|
|
655
|
+
for (let i = 0; i < segs.length && parent !== null && typeof parent === 'object'; i++) parent = parent[segs[i].replace(/~1/g, '/').replace(/~0/g, '~')];
|
|
656
|
+
}
|
|
657
|
+
let key = instancePath.slice(cut + 1).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
658
|
+
if (Array.isArray(parent) && /^(0|[1-9][0-9]*)$/.test(key)) key = +key;
|
|
659
|
+
return { instancePath, parentData: parent, parentDataProperty: key, rootData: root, schemaPath: schemaPath + '/' + keyword };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function isDiscriminated(schema) {
|
|
663
|
+
const d = schema.discriminator;
|
|
664
|
+
return d !== null && typeof d === 'object' && typeof d.propertyName === 'string';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// The reference's coercion: the first type in the list the value converts to,
|
|
668
|
+
// after unwrapping a one-element array in 'array' mode, which then converts
|
|
669
|
+
// the element.
|
|
670
|
+
const NO_COERCION = Symbol('no coercion');
|
|
671
|
+
function fitsType(d, t) {
|
|
672
|
+
switch (t) {
|
|
673
|
+
case 'string': return typeof d === 'string';
|
|
674
|
+
case 'number': return typeof d === 'number' && isFinite(d);
|
|
675
|
+
case 'integer': return typeof d === 'number' && Number.isInteger(d);
|
|
676
|
+
case 'boolean': return typeof d === 'boolean';
|
|
677
|
+
case 'null': return d === null;
|
|
678
|
+
case 'array': return Array.isArray(d);
|
|
679
|
+
case 'object': return d !== null && typeof d === 'object' && !Array.isArray(d);
|
|
680
|
+
default: return false;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// Whether an unwrapped element is kept as it is. The reference tests a list
|
|
684
|
+
// of types here with every type at once, so a list of two or more keeps
|
|
685
|
+
// nothing (`number` stands in for `integer` beside it), and the element goes
|
|
686
|
+
// on to be converted, as one of a single type does when it does not fit.
|
|
687
|
+
function unwrappedFits(d, types) {
|
|
688
|
+
let list = types;
|
|
689
|
+
if (list.length > 1 && list.includes('number')) list = list.filter((t) => t !== 'integer');
|
|
690
|
+
if (list.length > 1 && list.includes('array') && list.includes('object')) return false;
|
|
691
|
+
for (const t of list) if (!fitsType(d, t)) return false;
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
function coerceValue(data, types, arrayMode) {
|
|
695
|
+
if (arrayMode && Array.isArray(data) && data.length === 1) {
|
|
696
|
+
data = data[0];
|
|
697
|
+
if (unwrappedFits(data, types)) return data;
|
|
698
|
+
}
|
|
699
|
+
const kind = data === null ? 'null' : typeof data;
|
|
700
|
+
for (const t of types) {
|
|
701
|
+
switch (t) {
|
|
702
|
+
case 'number':
|
|
703
|
+
case 'integer':
|
|
704
|
+
if (kind === 'boolean' || kind === 'null') return +data;
|
|
705
|
+
// eslint-disable-next-line eqeqeq
|
|
706
|
+
if (kind === 'string' && data !== '' && data == +data && (t === 'number' || !(data % 1))) return +data;
|
|
707
|
+
break;
|
|
708
|
+
case 'string':
|
|
709
|
+
if (kind === 'number' || kind === 'boolean') return '' + data;
|
|
710
|
+
if (kind === 'null') return '';
|
|
711
|
+
break;
|
|
712
|
+
case 'boolean':
|
|
713
|
+
if (data === 'false' || data === 0 || data === null) return false;
|
|
714
|
+
if (data === 'true' || data === 1) return true;
|
|
715
|
+
break;
|
|
716
|
+
case 'null':
|
|
717
|
+
if (data === '' || data === 0 || data === false) return null;
|
|
718
|
+
break;
|
|
719
|
+
case 'array':
|
|
720
|
+
if (arrayMode && (kind === 'string' || kind === 'number' || kind === 'boolean' || kind === 'null')) return [data];
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return NO_COERCION;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// After a context-reading keyword, the value is read again from where it sits:
|
|
728
|
+
// the keyword may have replaced it (a date deserialized, a value serialized),
|
|
729
|
+
// and the keywords after it check what it left, as the default validator's
|
|
730
|
+
// do after a modifying keyword.
|
|
731
|
+
function reread(ctx, data) {
|
|
732
|
+
const p = ctx.parentData;
|
|
733
|
+
return p !== null && typeof p === 'object' && ctx.parentDataProperty !== undefined ? p[ctx.parentDataProperty] : data;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Every branch's result, kept on a collapsed anyOf or oneOf error when the
|
|
737
|
+
// schema has keywords that read their data's context. Such a keyword may have
|
|
738
|
+
// changed the data, so the errors of each branch cannot be had afterwards by
|
|
739
|
+
// validating it again; compat reads them from here (see compat-errors.js).
|
|
740
|
+
function keepBranches(error, branches) {
|
|
741
|
+
Object.defineProperty(error, '_branches', { value: branches });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function runCustomV(P, data, bits, instancePath, schemaPath) {
|
|
614
745
|
const ops = P.custom;
|
|
615
746
|
for (let i = 0; i < ops.length; i++) {
|
|
616
747
|
const op = ops[i];
|
|
617
748
|
if ((bits & op.mask) === 0) continue;
|
|
618
749
|
const fn = op.fn;
|
|
619
|
-
|
|
750
|
+
let ok;
|
|
751
|
+
if (op.ctx) {
|
|
752
|
+
const ctx = dataCtx(instancePath || '', schemaPath || '#', op.keyword);
|
|
753
|
+
ok = fn(data, ctx);
|
|
754
|
+
const now = reread(ctx, data);
|
|
755
|
+
if (now !== data) { data = now; bits = dataBits(data); }
|
|
756
|
+
} else ok = fn(data);
|
|
620
757
|
const holder = fn.source || fn;
|
|
621
758
|
if (holder.errors) holder.errors = null;
|
|
622
759
|
if (!ok) return false;
|
|
@@ -624,14 +761,23 @@ function runCustomV(P, data, bits) {
|
|
|
624
761
|
return true;
|
|
625
762
|
}
|
|
626
763
|
|
|
627
|
-
|
|
764
|
+
// `post`: undefined runs every keyword, false those not declared post, true
|
|
765
|
+
// only those.
|
|
766
|
+
function runCustom(P, data, bits, errors, instancePath, schemaPath, post) {
|
|
628
767
|
const ops = P.custom;
|
|
629
768
|
let valid = true;
|
|
630
769
|
for (let i = 0; i < ops.length; i++) {
|
|
631
770
|
const op = ops[i];
|
|
632
771
|
if ((bits & op.mask) === 0) continue;
|
|
772
|
+
if (post !== undefined && op.post !== post) continue;
|
|
633
773
|
const fn = op.fn;
|
|
634
|
-
|
|
774
|
+
let ok;
|
|
775
|
+
if (op.ctx) {
|
|
776
|
+
const ctx = dataCtx(instancePath, schemaPath, op.keyword);
|
|
777
|
+
ok = fn(data, ctx);
|
|
778
|
+
const now = reread(ctx, data);
|
|
779
|
+
if (now !== data) { data = now; bits = dataBits(data); }
|
|
780
|
+
} else ok = fn(data);
|
|
635
781
|
const holder = fn.source || fn;
|
|
636
782
|
const own = holder.errors;
|
|
637
783
|
if (own) holder.errors = null;
|
|
@@ -642,8 +788,10 @@ function runCustom(P, data, bits, errors, instancePath, schemaPath) {
|
|
|
642
788
|
if (Array.isArray(own) && own.length > 0) {
|
|
643
789
|
for (let j = 0; j < own.length; j++) {
|
|
644
790
|
const e = own[j];
|
|
791
|
+
// A context-reading keyword writes the path it was given, as the
|
|
792
|
+
// default validator's keywords do; the others write one relative to it.
|
|
645
793
|
errors.push(err(op.keyword, e.keyword || op.keyword,
|
|
646
|
-
e.instancePath !== undefined ? instancePath + e.instancePath : instancePath,
|
|
794
|
+
e.instancePath !== undefined ? (op.ctx ? e.instancePath : instancePath + e.instancePath) : instancePath,
|
|
647
795
|
e.schemaPath !== undefined ? e.schemaPath : kwPath,
|
|
648
796
|
e.params !== undefined ? e.params : {},
|
|
649
797
|
e.message !== undefined ? e.message : `must pass "${op.keyword}" keyword validation`));
|
|
@@ -681,6 +829,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
|
681
829
|
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
830
|
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
831
|
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; }
|
|
832
|
+
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
833
|
}
|
|
685
834
|
if (P.hasString && bits === T_STRING) {
|
|
686
835
|
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; }
|
|
@@ -731,6 +880,7 @@ class Interpreter {
|
|
|
731
880
|
this.root = rootSchema;
|
|
732
881
|
this.state = indexSchemas(rootSchema, opts.schemaMap, opts.flat === true);
|
|
733
882
|
this.userFormats = opts.formats || null;
|
|
883
|
+
this.numberFormats = opts.numberFormats && Object.keys(opts.numberFormats).length > 0 ? opts.numberFormats : null;
|
|
734
884
|
// Custom keywords, already normalized by lib/keywords.js.
|
|
735
885
|
this.keywords = opts.keywords || null;
|
|
736
886
|
// 2020-12 requires bookending for `$dynamicRef`; v1 removes the
|
|
@@ -744,6 +894,24 @@ class Interpreter {
|
|
|
744
894
|
this._fast = undefined;
|
|
745
895
|
this._calls = 0;
|
|
746
896
|
this._tier = typeof opts.tier === 'number' ? opts.tier : TIER;
|
|
897
|
+
// A keyword that asks for its data's context (parentData and the rest)
|
|
898
|
+
// needs every value's path, which only the collecting walk builds: the
|
|
899
|
+
// verdict comes from that walk too, and the closure tree is not used.
|
|
900
|
+
this.dataContext = false;
|
|
901
|
+
if (this.keywords !== null) for (const k of Object.keys(this.keywords)) if (this.keywords[k].dataContext) { this.dataContext = true; break; }
|
|
902
|
+
// Type coercion where each `type` is checked, the reference's way, which
|
|
903
|
+
// reaches a value behind allOf, anyOf, oneOf and $ref (the pass that runs
|
|
904
|
+
// before validation reaches `properties` and `items` only). It writes the
|
|
905
|
+
// coerced value where the value sits, so it takes the same walk.
|
|
906
|
+
this.coerce = opts.coerce === 'array' ? 'array' : opts.coerce === true;
|
|
907
|
+
if (this.coerce !== false) this.dataContext = true;
|
|
908
|
+
// OpenAPI's discriminator, read the reference's way under its
|
|
909
|
+
// `discriminator` option: the tag picks the one oneOf branch to apply.
|
|
910
|
+
this.discriminator = opts.discriminator === true;
|
|
911
|
+
this._discr = null;
|
|
912
|
+
// The compiled closures know none of these.
|
|
913
|
+
if (this.dataContext || this.discriminator || this.numberFormats !== null) this._fast = null;
|
|
914
|
+
this._ctxRoot = undefined;
|
|
747
915
|
}
|
|
748
916
|
|
|
749
917
|
// The closure-tree compiler covers every schema the interpreter accepts;
|
|
@@ -848,6 +1016,7 @@ class Interpreter {
|
|
|
848
1016
|
|
|
849
1017
|
// Verdict only: same walk as validate(), no error construction at all.
|
|
850
1018
|
isValid(data) {
|
|
1019
|
+
if (this.dataContext) return this.validate(data).valid;
|
|
851
1020
|
const fast = this._fast !== undefined ? this._fast : this._tiered();
|
|
852
1021
|
if (fast !== null) return fast.v(data, fast.cyclic ? [] : null);
|
|
853
1022
|
const dynScope = [this.state.rootBase];
|
|
@@ -859,6 +1028,83 @@ class Interpreter {
|
|
|
859
1028
|
}
|
|
860
1029
|
}
|
|
861
1030
|
|
|
1031
|
+
// The discriminator in place of oneOf: the branch the tag names is applied,
|
|
1032
|
+
// and a tag that is not a string, or names no branch, is the error.
|
|
1033
|
+
discriminate(P, data, base, dynScope, errors, instancePath, schemaPath, stack, local) {
|
|
1034
|
+
const d = this.discrMap(P, base);
|
|
1035
|
+
const tag = data[d.tagName];
|
|
1036
|
+
if (typeof tag !== 'string') {
|
|
1037
|
+
if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'tag', tag: d.tagName, tagValue: tag }, `tag "${d.tagName}" must be string`));
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
const i = d.map.get(tag);
|
|
1041
|
+
if (i === undefined) {
|
|
1042
|
+
if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'mapping', tag: d.tagName, tagValue: tag }, `value of tag "${d.tagName}" must be in oneOf`));
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
const sub = local !== DISCARD ? { props: null, items: null } : DISCARD;
|
|
1046
|
+
const ok = this.eval(P.oneOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/oneOf/' + i, stack, sub);
|
|
1047
|
+
if (ok && local !== DISCARD) mergeAnnotations(local, sub);
|
|
1048
|
+
return ok;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Tag value to branch index, read from each branch's `properties`, through
|
|
1052
|
+
// a `$ref`, with the reference's rules: a `const` or `enum` of unique
|
|
1053
|
+
// strings, and the tag required by the node or by every branch.
|
|
1054
|
+
discrMap(P, base) {
|
|
1055
|
+
if (this._discr === null) this._discr = new Map();
|
|
1056
|
+
let d = this._discr.get(P);
|
|
1057
|
+
if (d !== undefined) return d;
|
|
1058
|
+
const tagName = P.schema.discriminator.propertyName;
|
|
1059
|
+
if (P.schema.discriminator.mapping !== undefined) throw new Error('discriminator: mapping is not supported');
|
|
1060
|
+
const hasRequired = (s) => s !== null && typeof s === 'object' && Array.isArray(s.required) && s.required.includes(tagName);
|
|
1061
|
+
let topRequired = hasRequired(P.schema);
|
|
1062
|
+
const map = new Map();
|
|
1063
|
+
for (let i = 0; i < P.oneOf.length; i++) {
|
|
1064
|
+
let node = P.oneOf[i];
|
|
1065
|
+
if (node !== null && typeof node === 'object' && node.ref !== null && node.ref !== undefined && (node.schema.properties === undefined)) {
|
|
1066
|
+
const r = this.resolveRefCached(node, base);
|
|
1067
|
+
if (r.child === undefined) throw new Error(`discriminator: cannot resolve $ref ${node.ref}`);
|
|
1068
|
+
node = r.child;
|
|
1069
|
+
}
|
|
1070
|
+
const sch = node !== null && typeof node === 'object' ? node.schema : undefined;
|
|
1071
|
+
const propSch = sch && sch.properties ? sch.properties[tagName] : undefined;
|
|
1072
|
+
if (propSch === null || typeof propSch !== 'object') throw new Error(`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`);
|
|
1073
|
+
topRequired = topRequired || hasRequired(sch);
|
|
1074
|
+
const values = propSch.const !== undefined ? [propSch.const] : Array.isArray(propSch.enum) ? propSch.enum : null;
|
|
1075
|
+
if (values === null) throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`);
|
|
1076
|
+
for (const v of values) {
|
|
1077
|
+
if (typeof v !== 'string' || map.has(v)) throw new Error(`discriminator: "${tagName}" values must be unique strings`);
|
|
1078
|
+
map.set(v, i);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (!topRequired) throw new Error(`discriminator: "${tagName}" must be required`);
|
|
1082
|
+
d = { tagName, map };
|
|
1083
|
+
this._discr.set(P, d);
|
|
1084
|
+
return d;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Coerces the value at `instancePath` to the node's type and writes it where
|
|
1088
|
+
// it sits; at the root, where it sits nowhere, the walk carries it on.
|
|
1089
|
+
coerceAt(P, data, instancePath) {
|
|
1090
|
+
const to = coerceValue(data, P.typeNames, this.coerce === 'array');
|
|
1091
|
+
if (to === NO_COERCION) return data;
|
|
1092
|
+
if (instancePath === '') this._ctxRoot = to;
|
|
1093
|
+
else {
|
|
1094
|
+
const ctx = dataCtx(instancePath, '', '');
|
|
1095
|
+
if (ctx.parentData !== null && typeof ctx.parentData === 'object') ctx.parentData[ctx.parentDataProperty] = to;
|
|
1096
|
+
}
|
|
1097
|
+
return to;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// The value at `instancePath` as it is now, after a subschema may have
|
|
1101
|
+
// coerced it.
|
|
1102
|
+
here(instancePath, data) {
|
|
1103
|
+
if (instancePath === '') return this._ctxRoot;
|
|
1104
|
+
const ctx = dataCtx(instancePath, '', '');
|
|
1105
|
+
return ctx.parentData !== null && typeof ctx.parentData === 'object' ? ctx.parentData[ctx.parentDataProperty] : data;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
862
1108
|
validate(data) {
|
|
863
1109
|
const fast = this._fast !== undefined ? this._fast : this._tiered();
|
|
864
1110
|
if (fast !== null) {
|
|
@@ -869,13 +1115,20 @@ class Interpreter {
|
|
|
869
1115
|
let errors = [];
|
|
870
1116
|
const dynScope = [this.state.rootBase];
|
|
871
1117
|
let valid;
|
|
1118
|
+
// The root a context-reading keyword resolves its parent from; kept
|
|
1119
|
+
// across a nested validate() its own function might make.
|
|
1120
|
+
const outerRoot = this._ctxRoot;
|
|
1121
|
+
if (this.dataContext) { this._ctxRoot = data; ctxInterp = this; }
|
|
872
1122
|
try {
|
|
873
1123
|
// Nothing reads the root's annotations, so they are discarded from the start.
|
|
874
1124
|
valid = this.eval(this.rootNode, data, this.state.rootBase, dynScope, errors, '', '#', [], DISCARD);
|
|
875
1125
|
} catch (e) {
|
|
1126
|
+
if (this.dataContext) throw e;
|
|
876
1127
|
const deep = this._deep(e);
|
|
877
1128
|
errors = [];
|
|
878
1129
|
valid = deep.c(data, errors, '', '#', deep.cyclic ? [] : null);
|
|
1130
|
+
} finally {
|
|
1131
|
+
if (this.dataContext) this._ctxRoot = outerRoot;
|
|
879
1132
|
}
|
|
880
1133
|
return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
|
|
881
1134
|
}
|
|
@@ -885,14 +1138,17 @@ class Interpreter {
|
|
|
885
1138
|
// `stack` breaks infinite $ref recursion on identical (schema, data) pairs.
|
|
886
1139
|
eval(P, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
|
|
887
1140
|
// 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
|
-
|
|
1141
|
+
// reads them, and concatenating two per child was most of its cost. A
|
|
1142
|
+
// keyword that reads its data's context needs them on every walk.
|
|
1143
|
+
const E = errors !== NOERRORS || this.dataContext;
|
|
890
1144
|
if (P === true) return true;
|
|
891
1145
|
if (P === false) {
|
|
892
1146
|
if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
893
1147
|
return false;
|
|
894
1148
|
}
|
|
895
|
-
|
|
1149
|
+
// The leaf check cannot coerce: it neither knows the value's place nor
|
|
1150
|
+
// hands a coerced value on.
|
|
1151
|
+
if (P.leaf && this.coerce === false) return evalLeaf(P, data, errors, instancePath, schemaPath);
|
|
896
1152
|
const schema = P.schema;
|
|
897
1153
|
|
|
898
1154
|
// Only a reference can lead evaluation back to a schema it is already
|
|
@@ -926,6 +1182,9 @@ class Interpreter {
|
|
|
926
1182
|
const collect = sink !== DISCARD || P.hasUnevaluated;
|
|
927
1183
|
const local = collect ? { props: null, items: null } : DISCARD;
|
|
928
1184
|
|
|
1185
|
+
// Coercion first, before anything reads the value, as the reference does it.
|
|
1186
|
+
if (this.coerce !== false && P.hasType && (dataBits(data) & P.typeMask) === 0) data = this.coerceAt(P, data, instancePath);
|
|
1187
|
+
|
|
929
1188
|
// $ref / $dynamicRef: in-place applicators, annotations flow through
|
|
930
1189
|
if (P.ref !== null) {
|
|
931
1190
|
const { child, base: refBase } = this.resolveRefCached(P, base);
|
|
@@ -977,6 +1236,7 @@ class Interpreter {
|
|
|
977
1236
|
else if (collect) mergeAnnotations(local, sub);
|
|
978
1237
|
}
|
|
979
1238
|
}
|
|
1239
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
980
1240
|
|
|
981
1241
|
const bits = dataBits(data);
|
|
982
1242
|
|
|
@@ -1003,6 +1263,7 @@ class Interpreter {
|
|
|
1003
1263
|
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
1264
|
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
1265
|
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; }
|
|
1266
|
+
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
1267
|
}
|
|
1007
1268
|
|
|
1008
1269
|
// strings
|
|
@@ -1018,7 +1279,7 @@ class Interpreter {
|
|
|
1018
1279
|
// them, so on a node that was not a leaf this walk skipped them and
|
|
1019
1280
|
// accepted what they reject; the plan compiler, which answered every call
|
|
1020
1281
|
// until calls were tiered, hid it.
|
|
1021
|
-
if (P.hasCustom && !runCustom(P, data, bits, errors, instancePath, schemaPath)) valid = false;
|
|
1282
|
+
if (P.hasCustom && !runCustom(P, data, bits, errors, instancePath, schemaPath, P.hasPost ? false : undefined)) valid = false;
|
|
1022
1283
|
|
|
1023
1284
|
// arrays
|
|
1024
1285
|
if (P.hasArray && bits === T_ARRAY) {
|
|
@@ -1096,7 +1357,10 @@ class Interpreter {
|
|
|
1096
1357
|
let evaluated = false;
|
|
1097
1358
|
const prop = props !== null ? props.get(key) : undefined;
|
|
1098
1359
|
if (prop !== undefined) {
|
|
1099
|
-
|
|
1360
|
+
// A declared property whose value is undefined is not checked, as the
|
|
1361
|
+
// generated code does not check it: no JSON document carries one, and
|
|
1362
|
+
// serialized the key is not there.
|
|
1363
|
+
if (data[key] !== undefined && !this.eval(prop.node, data[key], base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
|
|
1100
1364
|
evaluated = true;
|
|
1101
1365
|
}
|
|
1102
1366
|
if (patterns !== null) {
|
|
@@ -1158,6 +1422,7 @@ class Interpreter {
|
|
|
1158
1422
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1159
1423
|
if (!this.eval(P.allOf[i], data, base, dynScope, errors, instancePath, E ? schemaPath + '/allOf/' + i : '', stack, sub)) valid = false;
|
|
1160
1424
|
else if (collect) mergeAnnotations(local, sub);
|
|
1425
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1161
1426
|
}
|
|
1162
1427
|
}
|
|
1163
1428
|
if (P.macros !== null) {
|
|
@@ -1186,16 +1451,25 @@ class Interpreter {
|
|
|
1186
1451
|
any = true;
|
|
1187
1452
|
if (collect) mergeAnnotations(local, sub);
|
|
1188
1453
|
}
|
|
1454
|
+
// Coercing, the branches after the first that passes are not tried,
|
|
1455
|
+
// as the reference does not try them: each could coerce the value.
|
|
1456
|
+
if (this.coerce !== false) {
|
|
1457
|
+
data = this.here(instancePath, data);
|
|
1458
|
+
if (ok && !collect) break;
|
|
1459
|
+
}
|
|
1189
1460
|
}
|
|
1190
1461
|
if (!any) {
|
|
1191
1462
|
if (branches !== null) {
|
|
1192
1463
|
const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
|
|
1464
|
+
if (collapsed !== null && this.dataContext) keepBranches(collapsed, branches);
|
|
1193
1465
|
if (collapsed !== null) errors.push(collapsed);
|
|
1194
1466
|
}
|
|
1195
1467
|
valid = false;
|
|
1196
1468
|
}
|
|
1197
1469
|
}
|
|
1198
|
-
if (P.oneOf !== null) {
|
|
1470
|
+
if (P.oneOf !== null && this.discriminator && isDiscriminated(schema)) {
|
|
1471
|
+
if (bits === T_OBJECT && !this.discriminate(P, data, base, dynScope, errors, instancePath, schemaPath, stack, collect ? local : DISCARD)) valid = false;
|
|
1472
|
+
} else if (P.oneOf !== null) {
|
|
1199
1473
|
let count = 0;
|
|
1200
1474
|
let winner = null;
|
|
1201
1475
|
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
@@ -1206,6 +1480,7 @@ class Interpreter {
|
|
|
1206
1480
|
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1207
1481
|
const mark = branches === null ? 0 : scratch.length;
|
|
1208
1482
|
const ok = this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, E ? schemaPath + '/oneOf/' + i : '', stack, sub);
|
|
1483
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1209
1484
|
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.oneOf[i]) });
|
|
1210
1485
|
if (ok) {
|
|
1211
1486
|
count++;
|
|
@@ -1216,6 +1491,7 @@ class Interpreter {
|
|
|
1216
1491
|
else {
|
|
1217
1492
|
if (branches !== null) {
|
|
1218
1493
|
const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
|
|
1494
|
+
if (collapsed !== null && this.dataContext) keepBranches(collapsed, branches);
|
|
1219
1495
|
if (collapsed !== null) errors.push(collapsed);
|
|
1220
1496
|
}
|
|
1221
1497
|
valid = false;
|
|
@@ -1227,11 +1503,13 @@ class Interpreter {
|
|
|
1227
1503
|
if (errors !== NOERRORS) errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
|
|
1228
1504
|
valid = false;
|
|
1229
1505
|
}
|
|
1506
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1230
1507
|
}
|
|
1231
1508
|
if (P.if !== undefined) {
|
|
1232
1509
|
const ifSub = collect ? { props: null, items: null } : DISCARD;
|
|
1233
1510
|
const ifScratch = errors === NOERRORS ? NOERRORS : [];
|
|
1234
1511
|
const ifOk = this.eval(P.if, data, base, dynScope, ifScratch, instancePath, E ? schemaPath + '/if' : '', stack, ifSub);
|
|
1512
|
+
if (this.coerce !== false) data = this.here(instancePath, data);
|
|
1235
1513
|
if (ifOk) {
|
|
1236
1514
|
if (collect) mergeAnnotations(local, ifSub);
|
|
1237
1515
|
if (P.then !== undefined) {
|
|
@@ -1281,6 +1559,11 @@ class Interpreter {
|
|
|
1281
1559
|
}
|
|
1282
1560
|
}
|
|
1283
1561
|
|
|
1562
|
+
if (P.hasPost) {
|
|
1563
|
+
if (this.dataContext) data = this.here(instancePath, data);
|
|
1564
|
+
if (!runCustom(P, data, dataBits(data), errors, instancePath, schemaPath, true)) valid = false;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1284
1567
|
if (scopePushed) dynScope.pop();
|
|
1285
1568
|
if (tracked) stack.length -= 2;
|
|
1286
1569
|
if (valid && collect && sink !== DISCARD) mergeAnnotations(sink, local);
|