ata-validator 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // Loading is delegated to lib/native-load.js so this file stays free of
4
4
  // platform probing and `path` (the browser entry must not pull those in).
5
5
  const native = require("./lib/native-load")();
6
+ const { normalizeKeywords, schemaUsesKeywords } = require('./lib/keywords');
6
7
  const {
7
8
  compileToJS,
8
9
  compileToJSCodegen,
@@ -790,6 +791,16 @@ class Validator {
790
791
  this._schemaIsCallers = schemaObj === schema;
791
792
  this._vocabulariesApplied = false;
792
793
 
794
+ // Custom keywords, normalized once. `_usesKeywords` is what routes the
795
+ // schema to the interpreted engine and keeps it out of the shared
796
+ // compile cache; a schema that registers keywords but uses none of them
797
+ // takes the ordinary path.
798
+ this._keywords = normalizeKeywords(options.keywords);
799
+ this._usesKeywords = false;
800
+ if (this._keywords !== null && schemaUsesKeywords(schemaObj, this._keywords)) {
801
+ this._usesKeywords = true;
802
+ }
803
+
793
804
  this._schemaStr = null; // lazy: computed on first use
794
805
  this._schemaObj = schemaObj;
795
806
  this._options = options;
@@ -918,7 +929,7 @@ class Validator {
918
929
  const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
919
930
  // Custom formats are JS functions: bypass the compile cache since they can
920
931
  // differ between validators that share the same schema string.
921
- const cached = this._userFormats ? null : _compileCache.get(mapKey);
932
+ const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
922
933
  let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
923
934
  var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
924
935
  // v1 removes the bookending requirement for $dynamicRef. Only the
@@ -936,7 +947,7 @@ class Validator {
936
947
  // survives the block and would quietly handle schemas it gets wrong; the
937
948
  // interpreted engine is both eval-free and more correct, so go straight
938
949
  // there.
939
- if (this._v1Dynamic || !codegenAvailable()) {
950
+ if (this._v1Dynamic || this._usesKeywords || !codegenAvailable()) {
940
951
  jsFn = null; jsCombinedFn = null; jsErrFn = null;
941
952
  } else if (cached && !_forceNapi) {
942
953
  jsFn = cached.jsFn;
@@ -1030,6 +1041,7 @@ class Validator {
1030
1041
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1031
1042
  formats: this._userFormats,
1032
1043
  v1: isV1Dialect(schemaObj),
1044
+ keywords: this._keywords,
1033
1045
  });
1034
1046
  }
1035
1047
  const r = _interp.validate(d);
@@ -1117,6 +1129,7 @@ class Validator {
1117
1129
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1118
1130
  formats: this._userFormats,
1119
1131
  v1: isV1Dialect(schemaObj),
1132
+ keywords: this._keywords,
1120
1133
  });
1121
1134
  }
1122
1135
  const interp = _interp;
@@ -1323,6 +1336,7 @@ class Validator {
1323
1336
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1324
1337
  formats: this._userFormats,
1325
1338
  v1: isV1Dialect(schemaObj),
1339
+ keywords: this._keywords,
1326
1340
  });
1327
1341
  this._engine = 'interpreter';
1328
1342
  _validate = (data) => interp.validate(data);
@@ -1385,6 +1399,7 @@ class Validator {
1385
1399
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1386
1400
  formats: this._userFormats,
1387
1401
  v1: isV1Dialect(schemaObj),
1402
+ keywords: this._keywords,
1388
1403
  });
1389
1404
  this._engine = 'interpreter';
1390
1405
  if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
@@ -1632,7 +1647,7 @@ class Validator {
1632
1647
  // every buffer entry point goes through validate() instead.
1633
1648
  if (native) {
1634
1649
  const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
1635
- if (bufferNeedsSlowPath(schemaObj, this._schemaMap)) installSlowBufferApis(this);
1650
+ if (bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords)) installSlowBufferApis(this);
1636
1651
  }
1637
1652
 
1638
1653
  // Save to identity cache for ultra-fast reuse with same schema object
@@ -1697,7 +1712,7 @@ class Validator {
1697
1712
  // A validator that rewrites its input cannot use the binding below: that
1698
1713
  // one answers from the compiled function alone and would skip the rewrite,
1699
1714
  // so isValidObject() and validate() would disagree.
1700
- if (this._needsPreprocess()) {
1715
+ if (this._needsPreprocess() || this._usesKeywords) {
1701
1716
  this._ensureCompiled();
1702
1717
  return;
1703
1718
  }
@@ -1708,7 +1723,7 @@ class Validator {
1708
1723
  const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
1709
1724
  // Custom formats are JS functions: skip the shared cache so different
1710
1725
  // validators with the same schema string but different formats don't collide.
1711
- const cached = this._userFormats ? null : _compileCache.get(mapKey);
1726
+ const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
1712
1727
  if (cached && cached.jsFn) {
1713
1728
  this._jsFn = cached.jsFn;
1714
1729
  this.isValidObject = cached.jsFn;
@@ -2131,8 +2146,11 @@ _defineLazyMethod('validate', (self) => (data) => {
2131
2146
  });
2132
2147
  _defineLazyMethod('isValidObject', (self) => (data) => {
2133
2148
  // A validator that rewrites its input goes through the full compile, which
2134
- // binds a verdict method that runs the rewrite first.
2135
- if (self._needsPreprocess()) {
2149
+ // binds a verdict method that runs the rewrite first. So does one whose
2150
+ // schema uses a custom keyword: neither the tier-0 plan nor the code
2151
+ // generator knows the keyword, and either would accept what validate()
2152
+ // rejects.
2153
+ if (self._needsPreprocess() || self._usesKeywords) {
2136
2154
  self._ensureCompiled();
2137
2155
  return self.isValidObject(data);
2138
2156
  }
package/lib/aot-impl.js CHANGED
@@ -58,6 +58,9 @@ function safeRePrelude(...fns) {
58
58
  // Generate a JS module string that can be written to a file.
59
59
  // On next startup, load with Validator.fromStandalone() -- zero compile time.
60
60
  function toStandalone(validator) {
61
+ if (validator._usesKeywords) {
62
+ throw new Error('toStandalone: a schema that uses custom keywords cannot be compiled into a standalone module (option "keywords")');
63
+ }
61
64
  validator._ensureCompiled();
62
65
  const jsFn = validator._jsFn;
63
66
  if (!jsFn || !jsFn._source) return null;
@@ -219,6 +222,9 @@ function emitClone(node, access, depth) {
219
222
  }
220
223
 
221
224
  function toStandaloneModule(validator, opts) {
225
+ if (validator._usesKeywords) {
226
+ throw new Error('toStandaloneModule: a schema that uses custom keywords cannot be compiled into a standalone module (option "keywords")');
227
+ }
222
228
  validator._ensureCompiled();
223
229
  const jsFn = validator._jsFn;
224
230
  if (!jsFn || !jsFn._source) return null;
@@ -351,6 +357,9 @@ ${parseCore}${exports}`;
351
357
  // // On startup:
352
358
  // const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
353
359
  function bundle(Validator, schemas, opts) {
360
+ if (opts && opts.keywords && Object.keys(opts.keywords).length > 0) {
361
+ throw new Error('bundle: custom keywords cannot be compiled into a standalone module (option "keywords")');
362
+ }
354
363
  const parts = schemas.map((schema) => {
355
364
  const v = new Validator(schema, opts);
356
365
  const standalone = toStandalone(v);
@@ -370,6 +379,12 @@ function bundle(Validator, schemas, opts) {
370
379
  // opts.format: 'cjs' (default) or 'esm'.
371
380
  // opts.formats: { name: fn } — embedded in the output via Function#toString.
372
381
  function bundleStandalone(Validator, schemas, opts) {
382
+ // A custom keyword is a function, and a standalone module imports nothing,
383
+ // so there is no way to carry it. Refuse rather than emit a module that
384
+ // silently ignores the keyword.
385
+ if (opts && opts.keywords && Object.keys(opts.keywords).length > 0) {
386
+ throw new Error('bundleStandalone: custom keywords cannot be compiled into a standalone module (option "keywords")');
387
+ }
373
388
  // Cross-schema $ref resolution: only meaningful when at least one schema has
374
389
  // an $id. Skip the schemas-as-map plumbing when none of them do.
375
390
  const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
@@ -436,6 +451,9 @@ function bundleStandalone(Validator, schemas, opts) {
436
451
  // Much smaller file → faster V8 parse → faster startup.
437
452
  // opts.format: 'cjs' (default) or 'esm'.
438
453
  function bundleCompact(Validator, schemas, opts) {
454
+ if (opts && opts.keywords && Object.keys(opts.keywords).length > 0) {
455
+ throw new Error('bundleCompact: custom keywords cannot be compiled into a standalone module (option "keywords")');
456
+ }
439
457
  const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
440
458
  const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
441
459
  const format = (opts && opts.format) || 'cjs';
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { schemaUsesKeywords } = require('./keywords');
4
+
3
5
  // The buffer APIs (isValid, countValid, batchIsValid, isValidNDJSON,
4
6
  // isValidParallel, isValidPrepadded, and isValidJSON above the simdjson
5
7
  // threshold) answer from the native engine's own walker, which is not the
@@ -77,7 +79,11 @@ function walk(schema, depth) {
77
79
  }
78
80
 
79
81
  // True when the buffer APIs must answer through validate() for this schema.
80
- function bufferNeedsSlowPath(schema, schemaMap) {
82
+ // `keywords` is the validator's normalized custom keyword map, if any: the
83
+ // native walker knows nothing about those, so a schema using one is routed
84
+ // through validate() like the other shapes here.
85
+ function bufferNeedsSlowPath(schema, schemaMap, keywords) {
86
+ if (keywords && schemaUsesKeywords(schema, keywords)) return true;
81
87
  if (walk(schema, 0)) return true;
82
88
  if (schemaMap && schemaMap.size > 0) {
83
89
  for (const s of schemaMap.values()) if (walk(s, 1)) return true;
@@ -0,0 +1,343 @@
1
+ 'use strict';
2
+
3
+ // Shapes an error list the way the reference validator reports it, for the
4
+ // compat entry. Two things differ between what validate() returns and what
5
+ // `require('ajv')` users read off `validate.errors`:
6
+ //
7
+ // Order. The reference does not follow schema declaration order; it runs
8
+ // keywords in a fixed order per node (the type check, the type-less
9
+ // keywords, then the number, string, array and object groups, each in the
10
+ // order in REFERENCE_ORDER), descending into a subschema when it reaches its
11
+ // applicator. Consumers that show only the first error see that order.
12
+ //
13
+ // Wrapper errors. Where a branch or a subschema fails, the reference reports
14
+ // the branch's own errors and then one for the keyword that combined them:
15
+ // every failing anyOf/oneOf branch, each failing property name under
16
+ // propertyNames, each item under contains (allErrors only), and the `if`
17
+ // keyword after a failed then/else (allErrors only). validate() reports the
18
+ // combining keyword alone. The shaper re-validates the branch or the value
19
+ // against the subschema to recover those errors, so nothing here guesses.
20
+ //
21
+ // With allErrors off the reference stops at the first failing keyword, but
22
+ // that keyword's wrapper group is reported whole: the branch errors and the
23
+ // anyOf, or the property name's error and the propertyNames. The same
24
+ // grouping is applied here after shaping.
25
+
26
+ const REFERENCE_ORDER = [
27
+ 'type',
28
+ '$dynamicAnchor', '$dynamicRef', '$recursiveAnchor', '$recursiveRef', '$comment', 'id', '$ref',
29
+ 'nullable', 'const', 'enum', 'not', 'anyOf', 'oneOf', 'allOf', 'if', 'then', 'else',
30
+ 'maximum', 'minimum', 'exclusiveMaximum', 'exclusiveMinimum', 'multipleOf',
31
+ 'maxLength', 'minLength', 'pattern', 'format',
32
+ 'maxItems', 'minItems', 'additionalItems', 'prefixItems', 'items', 'contains', 'uniqueItems', 'maxContains', 'minContains', 'unevaluatedItems',
33
+ 'maxProperties', 'minProperties', 'required', 'propertyNames', 'additionalProperties', 'dependencies', 'dependentRequired', 'dependentSchemas', 'properties', 'patternProperties', 'unevaluatedProperties',
34
+ ];
35
+ const REFERENCE_RANK = new Map(REFERENCE_ORDER.map((k, i) => [k, i]));
36
+
37
+ // Keywords whose next path segment is a user-chosen name or an index rather
38
+ // than a keyword.
39
+ const NAMED_SEGMENT = new Set(['properties', 'patternProperties', 'dependencies', 'dependentSchemas', 'dependentRequired', '$defs', 'definitions', 'propertyDependencies']);
40
+ const INDEXED_SEGMENT = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems', 'items', 'additionalItems']);
41
+
42
+ function unescape(seg) {
43
+ return seg.replace(/~1/g, '/').replace(/~0/g, '~');
44
+ }
45
+
46
+ function pointerOf(schemaPath) {
47
+ const hash = schemaPath.indexOf('#');
48
+ return hash >= 0 ? schemaPath.slice(hash + 1) : schemaPath;
49
+ }
50
+
51
+ function segments(pointer) {
52
+ return pointer === '' ? [] : pointer.split('/').slice(1);
53
+ }
54
+
55
+ // Walks a pointer's segments and reports, per position, whether that segment
56
+ // is a keyword or a name/index.
57
+ function classify(segs) {
58
+ const kinds = new Array(segs.length);
59
+ let skip = false;
60
+ for (let i = 0; i < segs.length; i++) {
61
+ if (skip) { kinds[i] = 'name'; skip = false; continue; }
62
+ kinds[i] = 'kw';
63
+ const s = segs[i];
64
+ if (NAMED_SEGMENT.has(s)) skip = true;
65
+ else if (INDEXED_SEGMENT.has(s) && /^\d+$/.test(segs[i + 1] || '')) skip = true;
66
+ }
67
+ return kinds;
68
+ }
69
+
70
+ function compareReferenceOrder(a, b) {
71
+ const pa = segments(pointerOf(a.schemaPath || ''));
72
+ const pb = segments(pointerOf(b.schemaPath || ''));
73
+ const ka = classify(pa);
74
+ const n = Math.min(pa.length, pb.length);
75
+ for (let i = 0; i < n; i++) {
76
+ if (pa[i] === pb[i]) continue;
77
+ if (ka[i] !== 'kw') return 0;
78
+ const ra = REFERENCE_RANK.get(pa[i]);
79
+ const rb = REFERENCE_RANK.get(pb[i]);
80
+ if (ra === undefined || rb === undefined) return 0;
81
+ return ra - rb;
82
+ }
83
+ return 0;
84
+ }
85
+
86
+ function sortLikeReference(errors) {
87
+ return errors.length < 2 ? errors : errors.slice().sort(compareReferenceOrder);
88
+ }
89
+
90
+ function walkPointer(node, pointer) {
91
+ for (const raw of segments(pointer)) {
92
+ if (node === null || typeof node !== 'object') return undefined;
93
+ const seg = unescape(raw);
94
+ node = Array.isArray(node) ? node[Number(seg)] : node[seg];
95
+ }
96
+ return node;
97
+ }
98
+
99
+ function walkData(data, instancePath) {
100
+ return walkPointer(data, instancePath);
101
+ }
102
+
103
+ function escapePointer(s) {
104
+ return s.replace(/~/g, '~0').replace(/\//g, '~1');
105
+ }
106
+
107
+ // The keywords whose failure the reference reports after the errors of
108
+ // what they combine. Custom keywords with a macro behave the same way and
109
+ // validate() already reports them so.
110
+ const WRAPPERS = new Set(['anyOf', 'oneOf', 'propertyNames', 'contains', 'not', 'if']);
111
+
112
+ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors, macroKeywords }) {
113
+ const branchCache = new Map();
114
+ const branchValidator = (pointer) => {
115
+ let v = branchCache.get(pointer);
116
+ if (!v) {
117
+ v = new Validator({ $ref: rootId + '#' + pointer }, options);
118
+ branchCache.set(pointer, v);
119
+ }
120
+ return v;
121
+ };
122
+ const wrappers = new Set(WRAPPERS);
123
+ for (const k of macroKeywords) wrappers.add(k);
124
+
125
+ // Errors whose schemaPath is already absolute (from the root), as opposed
126
+ // to relative to the branch validator that produced them.
127
+ const absolute = new WeakSet();
128
+
129
+ // Returns the shaped errors of `value` against the subschema at `pointer`
130
+ // (absolute), with schemaPath made absolute and instancePath rebased onto
131
+ // the caller's.
132
+ function subErrors(pointer, value, schemaPrefix, instancePrefix) {
133
+ const r = branchValidator(pointer).validate(value);
134
+ if (r.valid) return null;
135
+ const shaped = shape(r.errors, value, pointer);
136
+ return shaped.map((e) => {
137
+ const out = {
138
+ ...e,
139
+ schemaPath: absolute.has(e) ? e.schemaPath : schemaPrefix + pointer + pointerOf(e.schemaPath || '#'),
140
+ instancePath: instancePrefix + (e.instancePath || ''),
141
+ };
142
+ absolute.add(out);
143
+ return out;
144
+ });
145
+ }
146
+
147
+ function keywordError(keyword, instancePath, schemaPath, params, message) {
148
+ return { instancePath, schemaPath, keyword, params, message };
149
+ }
150
+
151
+ // `basePointer` is where `errors` were produced relative to the root
152
+ // document; a branch validator reports paths relative to its branch.
153
+ function shape(errors, data, basePointer) {
154
+ const sorted = sortLikeReference(errors);
155
+ const out = [];
156
+ const consumed = new Set();
157
+ let thenGroup = null; // { prefix, which } for the pending `if` wrapper
158
+
159
+ const flushIf = (e) => {
160
+ if (thenGroup === null) return;
161
+ const segs = segments(pointerOf(e ? e.schemaPath : ''));
162
+ const still = e && segs.length > thenGroup.depth && segs[thenGroup.depth] === thenGroup.which &&
163
+ segs.slice(0, thenGroup.depth).join('/') === thenGroup.prefixSegs;
164
+ if (still) return;
165
+ const ip = thenGroup.instancePath;
166
+ out.push(keywordError('if', ip, thenGroup.schemaPrefix + '/if', { failingKeyword: thenGroup.which }, `must match "${thenGroup.which}" schema`));
167
+ thenGroup = null;
168
+ };
169
+
170
+ for (let idx = 0; idx < sorted.length; idx++) {
171
+ const e = sorted[idx];
172
+ if (consumed.has(e)) continue;
173
+ const rel = pointerOf(e.schemaPath || '#');
174
+ const abs = basePointer + rel;
175
+ const segs = segments(rel);
176
+ const kinds = classify(segs);
177
+ const schemaPrefix = (e.schemaPath || '#').slice(0, (e.schemaPath || '#').indexOf('#') + 1);
178
+
179
+ if (allErrors) flushIf(e);
180
+
181
+ // The `if` wrapper: errors from a then/else subschema are followed by
182
+ // one for `if` (allErrors only, the reference returns before it
183
+ // otherwise).
184
+ if (allErrors && thenGroup === null) {
185
+ for (let i = 0; i < segs.length; i++) {
186
+ if (kinds[i] === 'kw' && (segs[i] === 'then' || segs[i] === 'else')) {
187
+ const prefixSegs = segs.slice(0, i);
188
+ thenGroup = {
189
+ depth: i, which: segs[i], prefixSegs: prefixSegs.join('/'),
190
+ schemaPrefix: schemaPrefix + (prefixSegs.length ? '/' + prefixSegs.join('/') : ''),
191
+ instancePath: e.instancePath,
192
+ };
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ const kw = e.keyword;
199
+
200
+ // anyOf / oneOf: report each failing branch's errors first.
201
+ if ((kw === 'anyOf' || kw === 'oneOf') && segs[segs.length - 1] === kw) {
202
+ const branches = walkPointer(rootDoc, abs);
203
+ if (Array.isArray(branches)) {
204
+ const value = walkData(data, e.instancePath || '');
205
+ const branchErrors = [];
206
+ const passing = [];
207
+ for (let i = 0; i < branches.length; i++) {
208
+ const errs = subErrors(abs + '/' + i, value, schemaPrefix, e.instancePath || '');
209
+ if (errs === null) passing.push(i);
210
+ else branchErrors.push(allErrors ? errs : errs.slice(0, groupEnd(errs)));
211
+ }
212
+ if (passing.length === 0) {
213
+ for (const errs of branchErrors) out.push(...errs);
214
+ out.push(e);
215
+ } else if (kw === 'oneOf') {
216
+ out.push({ ...e, params: { passingSchemas: passing }, message: 'must match exactly one schema in oneOf' });
217
+ } else {
218
+ out.push(e);
219
+ }
220
+ continue;
221
+ }
222
+ }
223
+
224
+ // propertyNames: the inner errors validate() reports carry no property
225
+ // name. Re-check each name so every failing one is reported with its
226
+ // own errors and a propertyNames error, as the reference does.
227
+ const pnIndex = segs.findIndex((s, i) => kinds[i] === 'kw' && s === 'propertyNames');
228
+ if (pnIndex >= 0 && pnIndex < segs.length - 1) {
229
+ const pnPointer = '/' + segs.slice(0, pnIndex + 1).join('/');
230
+ const pnAbs = basePointer + pnPointer;
231
+ const object = walkData(data, e.instancePath || '');
232
+ if (object && typeof object === 'object' && !Array.isArray(object)) {
233
+ // Everything validate() said about this propertyNames node at this
234
+ // instance is replaced by the regenerated sequence.
235
+ for (let j = idx; j < sorted.length; j++) {
236
+ const o = sorted[j];
237
+ if ((o.instancePath || '') === (e.instancePath || '') && pointerOf(o.schemaPath || '#').startsWith(pnPointer + '/')) consumed.add(o);
238
+ }
239
+ const pnSchemaPath = schemaPrefix + pnPointer;
240
+ for (const name of Object.keys(object)) {
241
+ const errs = subErrors(pnAbs, name, schemaPrefix, e.instancePath || '');
242
+ if (errs === null) continue;
243
+ const kept = allErrors ? errs : errs.slice(0, 1);
244
+ for (const k of kept) out.push({ ...k, instancePath: e.instancePath || '', propertyName: name });
245
+ out.push(keywordError('propertyNames', e.instancePath || '', pnSchemaPath, { propertyName: name }, 'property name must be valid'));
246
+ if (!allErrors) break;
247
+ }
248
+ continue;
249
+ }
250
+ }
251
+
252
+ // contains (allErrors only): each item's errors, then the keyword's.
253
+ if (kw === 'contains' && allErrors && segs[segs.length - 1] === 'contains') {
254
+ const items = walkData(data, e.instancePath || '');
255
+ if (Array.isArray(items)) {
256
+ for (let i = 0; i < items.length; i++) {
257
+ const errs = subErrors(abs, items[i], schemaPrefix, (e.instancePath || '') + '/' + i);
258
+ if (errs !== null) out.push(...errs);
259
+ }
260
+ out.push(e);
261
+ continue;
262
+ }
263
+ }
264
+
265
+ // draft-07 `dependencies`, which validate() reads as dependentRequired
266
+ // and dependentSchemas: the reference names the original keyword.
267
+ const depIndex = segs.findIndex((s, i) => kinds[i] === 'kw' && (s === 'dependentRequired' || s === 'dependentSchemas'));
268
+ if (depIndex >= 0) {
269
+ const nodePointer = basePointer + (depIndex === 0 ? '' : '/' + segs.slice(0, depIndex).join('/'));
270
+ const node = walkPointer(rootDoc, nodePointer);
271
+ if (node && typeof node === 'object' && node.dependencies && typeof node.dependencies === 'object') {
272
+ const rewritten = segs.slice();
273
+ rewritten[depIndex] = 'dependencies';
274
+ const schemaPath = schemaPrefix + '/' + rewritten.join('/');
275
+ if (segs[depIndex] === 'dependentRequired' && kw === 'required') {
276
+ const missing = e.params && e.params.missingProperty;
277
+ const object = walkData(data, e.instancePath || '');
278
+ let property;
279
+ for (const [prop, deps] of Object.entries(node.dependencies)) {
280
+ if (Array.isArray(deps) && deps.includes(missing) && object && Object.hasOwn(object, prop)) { property = prop; break; }
281
+ }
282
+ const deps = property !== undefined ? node.dependencies[property] : [missing];
283
+ out.push({
284
+ ...e,
285
+ keyword: 'dependencies',
286
+ schemaPath,
287
+ params: { property, missingProperty: missing, depsCount: deps.length, deps: deps.join(', ') },
288
+ message: `must have ${deps.length === 1 ? 'property' : 'properties'} ${deps.join(', ')} when property ${property} is present`,
289
+ });
290
+ } else {
291
+ out.push({ ...e, schemaPath });
292
+ }
293
+ continue;
294
+ }
295
+ }
296
+
297
+ out.push(e);
298
+ }
299
+ if (allErrors) flushIf(null);
300
+ return out;
301
+ }
302
+
303
+ // Index just past the first error's wrapper group: the first error, plus
304
+ // the errors that share the path of the outermost wrapper reported later
305
+ // for it (the sibling branches and the anyOf, the name's errors and the
306
+ // propertyNames, a macro's schema errors and the macro).
307
+ function groupEnd(shaped) {
308
+ if (shaped.length < 2) return shaped.length;
309
+ const first = shaped[0];
310
+ const firstPath = pointerOf(first.schemaPath || '#');
311
+ let end = 1;
312
+ let wrapperPath = null;
313
+ for (let i = 1; i < shaped.length; i++) {
314
+ const e = shaped[i];
315
+ const p = pointerOf(e.schemaPath || '#');
316
+ if (!wrappers.has(e.keyword)) continue;
317
+ if (firstPath === p || firstPath.startsWith(p + '/')) {
318
+ if (wrapperPath === null || p.length < wrapperPath.length) { wrapperPath = p; end = i + 1; }
319
+ }
320
+ }
321
+ if (wrapperPath === null) return 1;
322
+ // Everything up to the wrapper that lives under its path belongs to
323
+ // the group; anything else reported in between is dropped, as the
324
+ // reference never reached it.
325
+ const group = [];
326
+ for (let i = 0; i < end; i++) {
327
+ const e = shaped[i];
328
+ const p = pointerOf(e.schemaPath || '#');
329
+ if (p === wrapperPath || p.startsWith(wrapperPath + '/')) group.push(e);
330
+ }
331
+ shaped.splice(0, shaped.length, ...group, ...shaped.slice(end));
332
+ return group.length;
333
+ }
334
+
335
+ return function shapeErrors(errors, data) {
336
+ const shaped = shape(errors, data, '');
337
+ if (allErrors) return shaped;
338
+ const n = groupEnd(shaped);
339
+ return shaped.slice(0, n);
340
+ };
341
+ }
342
+
343
+ module.exports = { createShaper, sortLikeReference, REFERENCE_ORDER };