@jarenjs/validate 0.8.4 → 0.34.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.
Files changed (53) hide show
  1. package/ARCHITECTURE.md +1131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +796 -2
  4. package/dist/types/array.d.ts +2 -0
  5. package/dist/types/bigint.d.ts +1 -0
  6. package/dist/types/combine.d.ts +1 -0
  7. package/dist/types/condition.d.ts +1 -0
  8. package/dist/types/content.d.ts +3 -0
  9. package/dist/types/data.d.ts +7 -0
  10. package/dist/types/dollar-data.d.ts +11 -0
  11. package/dist/types/dynamic-ref.d.ts +44 -0
  12. package/dist/types/enum.d.ts +1 -0
  13. package/dist/types/format.d.ts +21 -0
  14. package/dist/types/index.d.ts +972 -0
  15. package/dist/types/messages.d.ts +142 -0
  16. package/dist/types/normalize.d.ts +107 -0
  17. package/dist/types/number.d.ts +1 -0
  18. package/dist/types/object.d.ts +3 -0
  19. package/dist/types/query-keyword.d.ts +19 -0
  20. package/dist/types/query.d.ts +29 -0
  21. package/dist/types/schema.d.ts +1 -0
  22. package/dist/types/string.d.ts +1 -0
  23. package/dist/types/tools.d.ts +109 -0
  24. package/dist/types/traverse.d.ts +32 -0
  25. package/dist/types/unevaluated.d.ts +12 -0
  26. package/docs/ERROR-MESSAGES.md +251 -0
  27. package/package.json +37 -7
  28. package/src/array.js +610 -0
  29. package/src/bigint.js +108 -0
  30. package/src/combine.js +276 -0
  31. package/src/condition.js +129 -0
  32. package/src/content.js +83 -0
  33. package/src/data.js +101 -0
  34. package/src/dollar-data.js +212 -0
  35. package/src/dynamic-ref.js +121 -0
  36. package/src/enum.js +147 -0
  37. package/src/format.js +108 -0
  38. package/src/index.js +1896 -0
  39. package/src/messages.js +497 -0
  40. package/src/normalize.js +585 -0
  41. package/src/number.js +169 -0
  42. package/src/object.js +848 -0
  43. package/src/query-keyword.js +99 -0
  44. package/src/query.js +85 -0
  45. package/src/schema.js +690 -0
  46. package/src/string.js +164 -0
  47. package/src/tools.js +397 -0
  48. package/src/traverse.js +442 -0
  49. package/src/unevaluated.js +173 -0
  50. package/dist/index.js +0 -1998
  51. package/dist/index.js.map +0 -7
  52. package/dist/index.min.js +0 -2
  53. package/dist/index.min.js.map +0 -7
@@ -0,0 +1,442 @@
1
+ import {
2
+ isBoolOrObjectClass,
3
+ hasSchemaRef,
4
+ } from './tools.js';
5
+
6
+ import {
7
+ isBoolishType,
8
+ } from '@jarenjs/core/number';
9
+
10
+ import {
11
+ isObjectClass,
12
+ isStringType,
13
+ } from '@jarenjs/core';
14
+
15
+ import {
16
+ isStringWhiteSpace,
17
+ } from '@jarenjs/core/string';
18
+
19
+ import {
20
+ isValidHtmlIdentifier,
21
+ } from '@jarenjs/core/text';
22
+
23
+ import {
24
+ encodeJSONPointerSegment,
25
+ decodeJSONPointerSegment,
26
+ } from '@jarenjs/json/pointer';
27
+
28
+
29
+ function encodeJsonPointerKey(key) {
30
+ return encodeURIComponent(encodeJSONPointerSegment(key));
31
+ }
32
+
33
+ export function encodeJsonPointerPath(path, key, index) {
34
+ return index == null
35
+ ? `${path}/${encodeJsonPointerKey(key)}`
36
+ : `${path}/${encodeJsonPointerKey(key)}/${encodeJsonPointerKey(String(index))}`;
37
+ }
38
+
39
+ function decodeJsonPointerKey(key) {
40
+ // Lenient decode: `$ref` fragments arrive here unvalidated, so a stray
41
+ // `~` must pass through instead of throwing like the strict parser does.
42
+ // The percent-decode is this site's own: these keys arrive from a URI
43
+ // fragment, which the plain reference-token decode knows nothing about.
44
+ return decodeJSONPointerSegment(decodeURIComponent(key));
45
+ }
46
+
47
+ export function decodeJsonPointerPath(path) {
48
+ // split and remove the leading empty string
49
+ return path.split('/').map(decodeJsonPointerKey).splice(1);
50
+ }
51
+
52
+ class JsonPointerOptions {
53
+ constructor(anchorsGlobal = false, anchorsAllowed = true, skipErrors = true) {
54
+ this.anchorsGlobal = anchorsGlobal;
55
+ this.anchorsAllowed = anchorsAllowed;
56
+ this.skipErrors = skipErrors;
57
+ }
58
+ }
59
+
60
+ class JsonPointer {
61
+ constructor(id, search, leftUri, fragment) {
62
+ this.id = id;
63
+ this.search = search;
64
+ this.leftUri = leftUri;
65
+ this.fragment = fragment;
66
+ }
67
+ }
68
+
69
+ export function createJsonPointer(refUri, baseUri, opts = new JsonPointerOptions()) {
70
+ let url;
71
+ try {
72
+ url = !isStringType(refUri) || isStringWhiteSpace(refUri)
73
+ ? new URL(baseUri)
74
+ : !isStringType(baseUri) || isStringWhiteSpace(baseUri)
75
+ ? new URL(refUri)
76
+ : new URL(refUri, baseUri);
77
+ } catch (e) {
78
+ // Handle case where baseUri is a relative reference (not a valid URL)
79
+ // Only apply manual resolution when baseUri is a plain identifier (no scheme, no /)
80
+ const isRelativeBase = isStringType(baseUri) && !/^[a-z][a-z0-9+.-]*:/i.test(baseUri);
81
+
82
+ if (isStringType(refUri) && isRelativeBase) {
83
+ // If refUri is absolute (has a scheme), use it as-is
84
+ if (/^[a-z][a-z0-9+.-]*:/i.test(refUri)) {
85
+ url = new URL(refUri);
86
+ } else if (refUri.startsWith('#')) {
87
+ // Fragment-only reference: combine with baseUri
88
+ const effectiveBase = baseUri;
89
+ url = new URL(refUri, 'http://example.com/' + effectiveBase);
90
+ // Restore the original baseUri in the result
91
+ const href = url.href.replace('http://example.com/', '');
92
+ const [, fragment] = href.split('#');
93
+ return new JsonPointer(
94
+ effectiveBase + refUri,
95
+ undefined,
96
+ effectiveBase + '#',
97
+ fragment || null
98
+ );
99
+ } else if (refUri.includes('#')) {
100
+ // refUri has a fragment: manual resolution
101
+ const [refBase, refFragment] = refUri.split('#');
102
+ const resolvedId = baseUri.endsWith('/')
103
+ ? baseUri + refBase + '#' + refFragment
104
+ : baseUri + '/' + refBase + '#' + refFragment;
105
+ return new JsonPointer(
106
+ resolvedId,
107
+ undefined,
108
+ baseUri + '#',
109
+ refFragment
110
+ );
111
+ } else {
112
+ // No fragment: simple concatenation
113
+ const resolvedId = baseUri.endsWith('/')
114
+ ? baseUri + refUri
115
+ : baseUri + '/' + refUri;
116
+ return new JsonPointer(resolvedId + '#', undefined, resolvedId + '#', null);
117
+ }
118
+ } else {
119
+ // Re-throw the original error if we can't handle it
120
+ throw e;
121
+ }
122
+ }
123
+
124
+ const [uri, fragment] = url.href.split('#');
125
+ const [leftUri, search] = uri.split('?');
126
+
127
+ if (!isStringWhiteSpace(fragment)) {
128
+ // this is a sort of $anchor and we are karen about it and thus not use isStringAnchor().
129
+ if (opts.anchorsAllowed == true && isValidHtmlIdentifier(fragment)) {
130
+ return opts.anchorsGlobal == true
131
+ ? new JsonPointer(`#${fragment}`, search, `${leftUri}#`, fragment)
132
+ : new JsonPointer(`${leftUri}#${fragment}`, search, `${leftUri}#`, fragment);
133
+ }
134
+ // or a json pointer
135
+ else if (fragment.startsWith('/')) {
136
+ return new JsonPointer(`${leftUri}#${fragment}`, search, `${leftUri}#`, fragment);
137
+ }
138
+ }
139
+ return new JsonPointer(`${leftUri}#`, search, `${leftUri}#`, null);
140
+ }
141
+
142
+ const TRAVERSE_SCHEMA_OBJECTS = [
143
+ 'items', 'prefixItems', 'additionalItems', 'contains', 'unevaluatedItems',
144
+ 'additionalProperties', 'propertyNames', 'unevaluatedProperties',
145
+ 'not', 'oneOf', 'anyOf', 'allOf', 'if', 'then', 'else',
146
+ ];
147
+ const TRAVERSE_SCHEMA_MAPS = [
148
+ 'properties', 'patternProperties',
149
+ 'dependencies', 'dependentSchemas', 'dependentRequired',
150
+ 'definitions', '$defs', 'components',
151
+ ];
152
+
153
+ export function storeSchemaIdsInMap(schemas, baseUri, schema, opts = new JsonPointerOptions()) {
154
+ if (!isObjectClass(schema)) {
155
+ const { id } = createJsonPointer(baseUri, undefined, opts);
156
+ schemas.set(id, schema);
157
+ return id;
158
+ }
159
+
160
+ const { id: rootUri } = createJsonPointer(schema.$id, baseUri, opts);
161
+ if (!isStringType(schema.$id) || isStringWhiteSpace(schema.$id)) {
162
+ if (schemas.has(rootUri))
163
+ throw new Error(`Schema '${rootUri}' already exists`);
164
+
165
+ schemas.set(rootUri, schema);
166
+ }
167
+
168
+ baseUri = rootUri;
169
+
170
+ const queue = [{ obj: schema, base: rootUri, path: '#' }];
171
+ while (queue.length > 0) {
172
+ // @ts-ignore
173
+ const { obj, base, path } = queue.shift();
174
+
175
+ // Handle $id (draft 6+) or id (draft 4) for identifying subschemas
176
+ const idKeyword = isStringType(obj.$id) ? obj.$id : isStringType(obj.id) ? obj.id : undefined;
177
+ if (isStringType(idKeyword) && !isStringWhiteSpace(idKeyword)) {
178
+ const { id } = createJsonPointer(idKeyword, base, opts);
179
+ if (!schemas.has(id))
180
+ schemas.set(id, obj);
181
+ else if (schemas.get(id) == null)
182
+ schemas.set(id, obj);
183
+ else
184
+ throw new Error(`Schema '${id}' for path '${path}' in '${base}' already exists`);
185
+
186
+ // CHECK: Also store under alternate ID (with/without # suffix) for absolute URIs only
187
+ if (!id.startsWith('#')) {
188
+ const altId = id.endsWith('#') ? id.slice(0, -1) : id + '#';
189
+ if (!schemas.has(altId)) {
190
+ schemas.set(altId, obj);
191
+ }
192
+ }
193
+
194
+ // we reset the baseUri when the $id property is set.
195
+ baseUri = id;
196
+ }
197
+ else
198
+ baseUri = base;
199
+
200
+ if (isStringType(obj.$anchor) && !isStringWhiteSpace(obj.$anchor)) {
201
+ const { id } = createJsonPointer(`#${obj.$anchor}`, baseUri, opts);
202
+ if (!schemas.has(id))
203
+ schemas.set(id, obj);
204
+ else if (schemas.get(id) == null)
205
+ schemas.set(id, obj);
206
+ else
207
+ throw new Error(`Schema '${id}' for path '${path}' in '${base}' already exists`);
208
+
209
+ // we reset the baseUri when the $anchor property is set.
210
+ if (!id.startsWith('#')) // except when anchors are global
211
+ baseUri = id;
212
+ }
213
+
214
+ // Handle $dynamicAnchor (draft 2020-12) - similar to $anchor but for $dynamicRef
215
+ if (isStringType(obj.$dynamicAnchor) && !isStringWhiteSpace(obj.$dynamicAnchor)) {
216
+ const { id } = createJsonPointer(`#${obj.$dynamicAnchor}`, baseUri, opts);
217
+ if (!schemas.has(id))
218
+ schemas.set(id, obj);
219
+ else if (schemas.get(id) == null)
220
+ schemas.set(id, obj);
221
+ else
222
+ throw new Error(`Schema '${id}' for path '${path}' in '${base}' already exists`);
223
+
224
+ // Note: $dynamicAnchor does NOT change the baseUri like $anchor does
225
+ // It's only used for $dynamicRef resolution
226
+ }
227
+
228
+ if (isStringType(obj.$ref) && !isStringWhiteSpace(obj.$ref)) {
229
+ const { id: ref } = createJsonPointer(obj.$ref, baseUri, opts);
230
+ if (!schemas.has(ref))
231
+ schemas.set(ref, null);
232
+
233
+ // Don't continue here - we need to process other schemas in the same
234
+ // parent object (like definitions) even if this one has a $ref.
235
+ // The $ref just means we don't traverse INTO this object's properties,
236
+ // but siblings should still be processed.
237
+ }
238
+
239
+ // iterate through all properties
240
+ for (const [key, value] of Object.entries(obj)) {
241
+ if (!isObjectClass(value) && !Array.isArray(value))
242
+ continue;
243
+
244
+ // is the property a schema object to traverse in?
245
+ if (TRAVERSE_SCHEMA_OBJECTS.includes(key)) {
246
+ if (Array.isArray(value)) {
247
+ const len = value.length;
248
+ for (let index = 0; index < len; index++) {
249
+ const item = value[index];
250
+ const nextpath = encodeJsonPointerPath(path, key, index);
251
+
252
+ if (isBoolishType(item))
253
+ continue;
254
+ if (!isObjectClass(item)) {
255
+ if (opts.skipErrors === true)
256
+ continue;
257
+ else
258
+ throw new Error(`${nextpath} is not a schema`);
259
+ }
260
+
261
+ queue.push({ obj: item, base: baseUri, path: nextpath });
262
+ }
263
+ }
264
+ else {
265
+ const nextpath = encodeJsonPointerPath(path, key);
266
+
267
+ queue.push({ obj: value, base: baseUri, path: nextpath });
268
+ }
269
+ }
270
+ // or is the property a map of key and schema objects?
271
+ else if (TRAVERSE_SCHEMA_MAPS.includes(key)) {
272
+ if (Array.isArray(value))
273
+ continue;
274
+
275
+ for (const [index, item] of Object.entries(value)) {
276
+ const nextpath = encodeJsonPointerPath(path, key, index);
277
+ if (isBoolishType(item))
278
+ continue;
279
+ if (!isObjectClass(item)) {
280
+ if (opts.skipErrors === true)
281
+ continue;
282
+ else
283
+ throw new Error(`${nextpath} is not a schema`);
284
+ }
285
+
286
+ queue.push({ obj: item, base: baseUri, path: nextpath });
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ return rootUri;
293
+ }
294
+
295
+ export function resolveRefSchemaShallow(schemas, refUri, baseUri, opts = new JsonPointerOptions()) {
296
+ let { id: base, leftUri, fragment } = createJsonPointer(refUri, baseUri, opts);
297
+ if (!schemas.has(leftUri))
298
+ throw new Error(`The root of reference: '$ref': '${base}', is not found in init-cache`);
299
+
300
+ // get the document from cache
301
+ let schema = schemas.get(leftUri);
302
+
303
+ // If there is no fragment, return the whole schema
304
+ if (isStringWhiteSpace(fragment)) {
305
+ return { id: base, schema };
306
+ }
307
+
308
+ // If the fragment is a plain name anchor (not a JSON pointer starting with /),
309
+ // look it up directly in the schemas map as a location-independent identifier
310
+ if (!fragment.startsWith('/')) {
311
+ // The anchor could be stored as just the fragment (e.g., "#foo") or as a full URI
312
+ // Try the full id first (which includes the base URI)
313
+ if (schemas.has(base)) {
314
+ return { id: base, schema: schemas.get(base) };
315
+ }
316
+ // Try the scoped anchor format: baseUri + fragment (e.g., "https://example.com/schema#foo")
317
+ // This handles anchors stored with anchorsGlobal: false
318
+ const scopedAnchorId = `${leftUri}${fragment}`;
319
+ if (schemas.has(scopedAnchorId)) {
320
+ return { id: scopedAnchorId, schema: schemas.get(scopedAnchorId) };
321
+ }
322
+ // Try just the fragment with hash (global anchor format)
323
+ const fragmentWithHash = `#${fragment}`;
324
+ if (schemas.has(fragmentWithHash)) {
325
+ return { id: fragmentWithHash, schema: schemas.get(fragmentWithHash) };
326
+ }
327
+ // Fall through to JSON pointer traversal for backward compatibility
328
+ }
329
+
330
+ // Decode and resolve the JSON pointer
331
+ const fragments = decodeJsonPointerPath(fragment);
332
+
333
+ // Traverse the schema based on the JSON pointer
334
+ let current = '';
335
+ for (const part of fragments) {
336
+ current = current + '/' + part;
337
+ if (!isBoolOrObjectClass(schema[part]) && !Array.isArray(schema[part]))
338
+ throw new Error(`The '${current}' is not is not a valid schema in '${leftUri}'`);
339
+
340
+ schema = schema[part];
341
+ if (isObjectClass(schema) && isStringType(schema.$id) && !isStringWhiteSpace(schema.$id)) {
342
+ const { id } = createJsonPointer(schema.$id, base, opts);
343
+ base = id;
344
+ }
345
+ }
346
+
347
+ return { id: base, schema };
348
+ }
349
+
350
+ export function restoreSchemaRefsInMap(schemas, opts = new JsonPointerOptions()) {
351
+ for (const [id, item] of schemas.entries()) {
352
+ if (item != null)
353
+ continue;
354
+
355
+ // Try to use resolveRefSchemaDeep to flatten ref chains
356
+ // This resolves a→b→c into a→c, eliminating chain traversal at validation time
357
+ // If deep resolution fails (e.g., remote ref not loaded yet), fall back to shallow
358
+ try {
359
+ const { id: finalId, schema: finalSchema } = resolveRefSchemaDeep(
360
+ schemas,
361
+ id,
362
+ { $ref: id },
363
+ opts
364
+ );
365
+
366
+ if (finalSchema == null)
367
+ throw new Error(`Can not resolve schema for '${id}'`);
368
+
369
+ // Store the final resolved schema (flattened ref chain)
370
+ schemas.set(id, finalSchema);
371
+
372
+ // Also store under the final ID for direct access if not already present
373
+ if (finalId !== id && !schemas.has(finalId)) {
374
+ schemas.set(finalId, finalSchema);
375
+ }
376
+ } catch (_e) {
377
+ // If deep resolution fails (remote ref not loaded), fall back to shallow resolution
378
+ // This preserves the original behavior for unresolved refs
379
+ const { schema } = resolveRefSchemaShallow(schemas, id, null, opts);
380
+ if (schema == null)
381
+ throw new Error(`Can not resolve schema for '${id}'`);
382
+
383
+ schemas.set(id, schema);
384
+ }
385
+ }
386
+ }
387
+
388
+ export class TraverseOptions extends JsonPointerOptions {
389
+ constructor(
390
+ origin = 'https://github.com/jklarenbeek/jarenjs',
391
+ mergeSchemas = true,
392
+ anchorsGlobal = false,
393
+ anchorsAllowed = true,
394
+ skipErrors = true
395
+ ) {
396
+ super(anchorsGlobal, anchorsAllowed, skipErrors);
397
+ this.origin = origin,
398
+ this.mergeSchemas = mergeSchemas;
399
+ }
400
+ }
401
+
402
+ export function resolveRefSchemaDeep(schemas, baseUri, refschema, opts = new TraverseOptions()) {
403
+ if (!isObjectClass(refschema))
404
+ return { id: baseUri, schema: refschema };
405
+
406
+ if (!hasSchemaRef(refschema))
407
+ return { id: baseUri, schema: refschema };
408
+
409
+ const queue = [{ item: refschema, base: baseUri }];
410
+ const seen = new Set();
411
+ let result = {};
412
+
413
+ while (queue.length > 0) {
414
+ // @ts-ignore
415
+ const { item, base } = queue.shift();
416
+ if (!isObjectClass(item))
417
+ return { id: base, schema: item };
418
+
419
+ // In draft 7 and earlier, $ref completely replaces the schema
420
+ // and all sibling keywords must be ignored. We only keep the $ref
421
+ // to resolve it, discarding all other keywords from this item.
422
+ if (hasSchemaRef(item)) {
423
+ const ref = item.$ref;
424
+ const { id, schema } = resolveRefSchemaShallow(schemas, ref, base, opts);
425
+
426
+ if (seen.has(id))
427
+ return { id: base, schema: result };
428
+
429
+ seen.add(id);
430
+
431
+ queue.push({ item: schema, base: id });
432
+ } else {
433
+ // No $ref in this item, merge as normal
434
+ result = opts.mergeSchemas == true
435
+ ? { ...item, ...result }
436
+ : { ...item };
437
+ return { id: base, schema: result };
438
+ }
439
+ }
440
+
441
+ throw new Error(`The json schema '${baseUri}' can not be resolved!`);
442
+ }
@@ -0,0 +1,173 @@
1
+ //@ts-check
2
+
3
+ import {
4
+ isObjectType,
5
+ isArrayClass,
6
+ } from '@jarenjs/core';
7
+
8
+ import {
9
+ getBoolOrObjectClass,
10
+ hasUnevaluatedPropertiesCoverage,
11
+ hasUnevaluatedItemsCoverage,
12
+ } from './tools.js';
13
+
14
+ /**
15
+ * Compile the unevaluatedProperties keyword as a final-stage validator.
16
+ * Receives the evaluation-log mark taken before the containing schema
17
+ * (including a sibling $ref) started, and checks every property of the
18
+ * instance that was not recorded as evaluated since that mark.
19
+ * @param {import('./index.js').ValidationObject} schemaObj
20
+ * @param {object} jsonSchema
21
+ * @returns {function|undefined} validator(data, dataPath, dataRoot, mark)
22
+ */
23
+ function compileUnevaluatedProperties(schemaObj, jsonSchema) {
24
+ const uneval = getBoolOrObjectClass(jsonSchema.unevaluatedProperties);
25
+ if (uneval == null) return undefined;
26
+
27
+ // In skipErrors mode reaching this final-stage check means every sibling
28
+ // keyword passed; with a sibling additionalProperties every property was
29
+ // then evaluated (and logged), so the check can never match and the
30
+ // sibling already produced the annotations any outer check consumes.
31
+ if (schemaObj.options.skipErrors && hasUnevaluatedPropertiesCoverage(jsonSchema))
32
+ return undefined;
33
+
34
+ const root = schemaObj.root;
35
+ const addError = schemaObj.createErrorHandler(uneval, 'unevaluatedProperties');
36
+
37
+ // true: everything left over is valid, but counts as evaluated for
38
+ // any unevaluatedProperties in an outer schema.
39
+ if (uneval === true) {
40
+ return function validateUnevaluatedPropertiesTrue(data, _dataPath, _dataRoot, _mark) {
41
+ if (!isObjectType(data)) return true;
42
+ const log = root.evalLog;
43
+ const keys = Object.keys(data);
44
+ for (let i = 0; i < keys.length; ++i) {
45
+ log.add(data, keys[i]);
46
+ }
47
+ return true;
48
+ };
49
+ }
50
+
51
+ if (uneval === false) {
52
+ return function validateUnevaluatedPropertiesFalse(data, dataPath, dataRoot, mark) {
53
+ if (!isObjectType(data)) return true;
54
+ const log = root.evalLog;
55
+ const keys = Object.keys(data);
56
+ let valid = true;
57
+ for (let i = 0; i < keys.length; ++i) {
58
+ if (!log.hasKey(data, keys[i], mark))
59
+ valid = addError(keys[i], dataPath) && valid;
60
+ }
61
+ return valid;
62
+ };
63
+ }
64
+
65
+ const validator = schemaObj.createValidator(uneval, 'unevaluatedProperties');
66
+ return function validateUnevaluatedProperties(data, dataPath, dataRoot, mark) {
67
+ if (!isObjectType(data)) return true;
68
+ const log = root.evalLog;
69
+ const keys = Object.keys(data);
70
+ let valid = true;
71
+ for (let i = 0; i < keys.length; ++i) {
72
+ const key = keys[i];
73
+ if (log.hasKey(data, key, mark)) continue;
74
+ if (validator(data[key], dataPath, dataRoot, key) === true)
75
+ log.add(data, key);
76
+ else
77
+ valid = addError(key, dataPath) && valid;
78
+ }
79
+ return valid;
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Compile the unevaluatedItems keyword as a final-stage validator.
85
+ * @param {import('./index.js').ValidationObject} schemaObj
86
+ * @param {object} jsonSchema
87
+ * @returns {function|undefined} validator(data, dataPath, dataRoot, mark)
88
+ */
89
+ function compileUnevaluatedItems(schemaObj, jsonSchema) {
90
+ const uneval = getBoolOrObjectClass(jsonSchema.unevaluatedItems);
91
+ if (uneval == null) return undefined;
92
+
93
+ // In skipErrors mode reaching this final-stage check means every sibling
94
+ // keyword passed; with sibling coverage (uniform items, or tuple items
95
+ // plus additionalItems) every item was then evaluated (and logged), so
96
+ // the check can never match and the covering sibling already produced
97
+ // the annotations any outer check consumes.
98
+ if (schemaObj.options.skipErrors && hasUnevaluatedItemsCoverage(jsonSchema))
99
+ return undefined;
100
+
101
+ const root = schemaObj.root;
102
+ const addError = schemaObj.createErrorHandler(uneval, 'unevaluatedItems');
103
+
104
+ if (uneval === true) {
105
+ return function validateUnevaluatedItemsTrue(data, _dataPath, _dataRoot, _mark) {
106
+ if (!isArrayClass(data)) return true;
107
+ root.evalLog.add(data, -1);
108
+ return true;
109
+ };
110
+ }
111
+
112
+ if (uneval === false) {
113
+ return function validateUnevaluatedItemsFalse(data, dataPath, dataRoot, mark) {
114
+ if (!isArrayClass(data)) return true;
115
+ const log = root.evalLog;
116
+ for (let i = 0; i < data.length; ++i) {
117
+ if (!log.hasItem(data, i, mark))
118
+ return addError(i, dataPath);
119
+ }
120
+ return true;
121
+ };
122
+ }
123
+
124
+ const validator = schemaObj.createValidator(uneval, 'unevaluatedItems');
125
+ return function validateUnevaluatedItems(data, dataPath, dataRoot, mark) {
126
+ if (!isArrayClass(data)) return true;
127
+ const log = root.evalLog;
128
+ let valid = true;
129
+ for (let i = 0; i < data.length; ++i) {
130
+ if (log.hasItem(data, i, mark)) continue;
131
+ if (validator(data[i], dataPath, dataRoot, i) === true)
132
+ log.add(data, i);
133
+ else
134
+ valid = addError(i, dataPath) && valid;
135
+ }
136
+ return valid;
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Wraps a compiled schema validator so unevaluatedProperties/unevaluatedItems
142
+ * run last, seeing every annotation produced on this instance by the schema's
143
+ * own keywords and its in-place applicators (allOf/anyOf/oneOf/if/$ref/...).
144
+ * Returns the validator unchanged when evaluation tracking is off or the
145
+ * schema has no unevaluated* keywords.
146
+ * @param {import('./index.js').ValidationObject} schemaObj
147
+ * @param {object} jsonSchema
148
+ * @param {function} validator - The compiled validator for all other keywords
149
+ * @returns {function} The wrapped (or original) validator
150
+ */
151
+ export function wrapUnevaluated(schemaObj, jsonSchema, validator) {
152
+ const root = schemaObj.root;
153
+ if (!root.usesUnevaluated) return validator;
154
+ if (jsonSchema == null || typeof jsonSchema !== 'object') return validator;
155
+
156
+ const unevalProps = compileUnevaluatedProperties(schemaObj, jsonSchema);
157
+ const unevalItems = compileUnevaluatedItems(schemaObj, jsonSchema);
158
+ if (unevalProps == null && unevalItems == null) return validator;
159
+
160
+ const stopAtFirst = root.options.skipErrors;
161
+ return function validateUnevaluatedSchema(data, dataPath, dataRoot, dataKey) {
162
+ const log = root.evalLog;
163
+ const mark = log.mark();
164
+ // The sibling result IS a precondition: unevaluated* reads annotations
165
+ // that a failed sibling may never have produced. But unevaluatedProperties
166
+ // and unevaluatedItems are independent of each other.
167
+ if (validator(data, dataPath, dataRoot, dataKey) === false) return false;
168
+ let valid = unevalProps == null || unevalProps(data, dataPath, dataRoot, mark) !== false;
169
+ if (stopAtFirst && !valid) return false;
170
+ if (unevalItems != null && unevalItems(data, dataPath, dataRoot, mark) === false) valid = false;
171
+ return valid;
172
+ };
173
+ }