@jarenjs/validate 0.8.3 → 0.9.2

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