ata-validator 1.52.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.
@@ -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
- function runCustomV(P, data, bits) {
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
- const ok = fn(data);
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
- function runCustom(P, data, bits, errors, instancePath, schemaPath) {
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
- const ok = fn(data);
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
- const E = errors !== NOERRORS;
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
- if (P.leaf) return evalLeaf(P, data, errors, instancePath, schemaPath);
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
- if (!this.eval(prop.node, data[key], base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
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);
@@ -376,7 +376,9 @@ function compileToJS(schema, defs, schemaMap) {
376
376
  if (!propCheck) return null // bail if sub-schema too complex
377
377
  checks.push((d) => {
378
378
  if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
379
- return propCheck(d[key])
379
+ // An undefined value is not checked, as the generated code does not.
380
+ const v = d[key]
381
+ return v === undefined || propCheck(v)
380
382
  })
381
383
  }
382
384
  }
package/lib/keywords.js CHANGED
@@ -10,6 +10,12 @@
10
10
  // { macro(value, parent) -> schema } the returned schema is applied in
11
11
  // place, and the keyword reports an
12
12
  // error of its own when it fails
13
+ // with optional `dataContext: true`: the per-value function is also given
14
+ // `{ instancePath, parentData, parentDataProperty, rootData }`, as the default
15
+ // validator's keywords are, so it can read or change the value where it sits
16
+ // (validate(value, data, parent, ctx), or the compiled fn(data, ctx)). Such a
17
+ // schema runs on the interpreted engine's collecting walk, which knows where
18
+ // every value is.
13
19
  // with optional `type`: a JSON Schema type name or a list of them. A typed
14
20
  // keyword is skipped for data of any other type, which is what makes
15
21
  // `{ type: 'number', ... }` safe next to `type: ['number', 'string']`.
@@ -72,6 +78,12 @@ function normalizeKeywords(defs) {
72
78
  validate: hasValidate ? def.validate : null,
73
79
  compile: hasCompile ? def.compile : null,
74
80
  macro: hasMacro ? def.macro : null,
81
+ dataContext: def.dataContext === true,
82
+ // Order among the custom keywords of one node, as the default
83
+ // validator's keywords declare it: `before` names a keyword this one
84
+ // runs ahead of, `post` runs it after the others.
85
+ before: typeof def.before === 'string' ? def.before : null,
86
+ post: def.post === true,
75
87
  };
76
88
  }
77
89
  return out;
@@ -110,4 +122,15 @@ const NAME_MAPS = new Set([
110
122
  'dependentSchemas', 'dependentRequired', 'dependencies', 'propertyDependencies',
111
123
  ]);
112
124
 
113
- module.exports = { normalizeKeywords, schemaUsesKeywords };
125
+ // Does the schema use a registered keyword that asks for its data's context?
126
+ function schemaUsesDataContext(schema, keywords) {
127
+ if (!keywords) return false;
128
+ let any = false;
129
+ for (const name of Object.keys(keywords)) if (keywords[name].dataContext) { any = true; break; }
130
+ if (!any) return false;
131
+ const only = Object.create(null);
132
+ for (const name of Object.keys(keywords)) if (keywords[name].dataContext) only[name] = keywords[name];
133
+ return schemaUsesKeywords(schema, only);
134
+ }
135
+
136
+ module.exports = { normalizeKeywords, schemaUsesKeywords, schemaUsesDataContext };
@@ -1,20 +1,25 @@
1
1
  'use strict';
2
2
 
3
- // Bounded Levenshtein. Returns Infinity if distance > maxDistance (or, past
3
+ // Bounded edit distance. Returns Infinity if distance > maxDistance (or, past
4
4
  // the bound, possibly some number greater than it; callers compare).
5
5
  //
6
+ // Two adjacent characters swapped count as one edit, not two (the optimal
7
+ // string alignment distance): `nmae` for `name` is the commonest typo there
8
+ // is, and at two edits a four-letter name's budget of one turned it away.
9
+ //
6
10
  // Only the cells within the bound of the diagonal are computed: a path
7
11
  // through any other cell already costs more than the bound. A typo hint asks
8
12
  // with a bound of one or two and compares the missing name with every key
9
13
  // present, so a row is five cells instead of the whole key. Without a bound
10
14
  // the band is the whole row.
11
15
  //
12
- // Two rows of scratch, reused across calls. The function is not recursive
16
+ // Three rows of scratch, reused across calls. The function is not recursive
13
17
  // and the library is single-threaded, so nothing else is using them; growing
14
18
  // them once beats allocating two arrays on every comparison, which the
15
19
  // reject-path profile showed as most of this function's cost.
16
20
  let scratchA = new Int32Array(64);
17
21
  let scratchB = new Int32Array(64);
22
+ let scratchC = new Int32Array(64);
18
23
 
19
24
  function levenshtein (a, b, maxDistance) {
20
25
  if (a === b) return 0;
@@ -28,7 +33,11 @@ function levenshtein (a, b, maxDistance) {
28
33
  if (scratchA.length < m + 2) {
29
34
  scratchA = new Int32Array(m + 2);
30
35
  scratchB = new Int32Array(m + 2);
36
+ scratchC = new Int32Array(m + 2);
31
37
  }
38
+ // The row before the previous one, read for a swap: within the band of
39
+ // row i the cell two back on the diagonal is inside the band of row i - 2.
40
+ let prev2 = scratchC;
32
41
  let prev = scratchA;
33
42
  let curr = scratchB;
34
43
  for (let j = 0; j <= m; j++) prev[j] = j <= max ? j : big;
@@ -40,20 +49,27 @@ function levenshtein (a, b, maxDistance) {
40
49
  // charCodeAt rather than indexing: reading a character by index allocates
41
50
  // a one-character string, and this is the innermost loop.
42
51
  const ca = a.charCodeAt(i - 1);
52
+ const pa = i > 1 ? a.charCodeAt(i - 2) : -1;
43
53
  for (let j = lo; j <= hi; j++) {
44
- let v = prev[j - 1] + (ca === b.charCodeAt(j - 1) ? 0 : 1);
54
+ const cb = b.charCodeAt(j - 1);
55
+ let v = prev[j - 1] + (ca === cb ? 0 : 1);
45
56
  const del = curr[j - 1] + 1;
46
57
  if (del < v) v = del;
47
58
  const ins = prev[j] + 1;
48
59
  if (ins < v) v = ins;
60
+ if (j > 1 && ca !== cb && ca === b.charCodeAt(j - 2) && pa === cb) {
61
+ const swap = prev2[j - 2] + 1;
62
+ if (swap < v) v = swap;
63
+ }
49
64
  if (v > big) v = big;
50
65
  curr[j] = v;
51
66
  if (v < rowMin) rowMin = v;
52
67
  }
53
68
  if (hi < m) curr[hi + 1] = big;
54
69
  if (rowMin > max) return Infinity;
55
- // A destructured swap builds an array per row; a temporary does not.
56
- const t = prev;
70
+ // A destructured swap builds an array per row; temporaries do not.
71
+ const t = prev2;
72
+ prev2 = prev;
57
73
  prev = curr;
58
74
  curr = t;
59
75
  }