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.
@@ -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
- if (!Object.hasOwn(data, req[i])) return false;
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
- function runCustomV(P, data, bits) {
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
- const ok = fn(data);
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
- function runCustom(P, data, bits, errors, instancePath, schemaPath) {
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
- const ok = fn(data);
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
- const E = errors !== NOERRORS;
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
- if (P.leaf) return evalLeaf(P, data, errors, instancePath, schemaPath);
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
- if (!this.eval(prop.node, data[key], base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
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
- function genGuardedC (sub, v, pathExpr, lines, ctx, schemaPrefix) {
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
- if (g.name !== undefined) lines.push(`if(!${g.name}(${v})){`)
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;g${gi}:{${replaceTopLevel(g.body, `{_g${gi}=0;break g${gi}}`, `break g${gi}`)};_g${gi}=0}if(_g${gi}){`)
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})&&!${ownKeyExpr(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}}`)
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
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
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
- return `(_e=_ap(_A,_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}),-1))`
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
- : hoistDecl + allDecl + `let _e;${AP_RESET}\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
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
- const factory = compileFunction('R' + (closureParams ? ',' + closureParams : ''), programSrc)
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` : `!${ownKeyExpr(ctx, v, key)}`
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})&&!${ownKeyExpr(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)}'"}`)}}`)
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
- lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
2647
- genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
2648
- lines.push(`}}`)
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