@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
package/src/schema.js ADDED
@@ -0,0 +1,690 @@
1
+ //@ts-check
2
+
3
+ import {
4
+ isObjectType,
5
+ getStringType,
6
+ isObjectClass,
7
+ } from '@jarenjs/core';
8
+ import {
9
+ NUMERIC_CONSTRAINTS, STRING_CONSTRAINTS,
10
+ ARRAY_CONSTRAINTS, OBJECT_CONSTRAINTS,
11
+ } from '@jarenjs/core/schema';
12
+
13
+ import {
14
+ getUniqueArray,
15
+ } from '@jarenjs/core/array';
16
+
17
+ import {
18
+ getBoolishType,
19
+ } from '@jarenjs/core/number';
20
+
21
+ import {
22
+ trueThat,
23
+ addFunctionToArray,
24
+ } from '@jarenjs/core/function';
25
+
26
+ import {
27
+ combineIndependent,
28
+ createIsSchemaTypeHandler,
29
+ hasSchemaRef,
30
+ hasSchemaRecursiveRef,
31
+ hasSchemaDynamicRef,
32
+ } from './tools.js';
33
+
34
+ import {
35
+ getStringLength,
36
+ } from '@jarenjs/core/string';
37
+
38
+ import { compileErrorMessageSpec } from './messages.js';
39
+ import { compileFormatBasic } from './format.js';
40
+ import { compileEnumBasic } from './enum.js';
41
+ import { compileNumberBasic } from './number.js';
42
+ import { compileBigIntBasic } from './bigint.js';
43
+ import { compileStringBasic } from './string.js';
44
+ import { compileContentSchema } from './content.js';
45
+ import { compileObjectSchema } from './object.js';
46
+ import { compileArraySchema } from './array.js';
47
+ import { compileCombineSchema } from './combine.js';
48
+ import { compileConditionSchema } from './condition.js';
49
+ import { compileDataSchema } from './data.js';
50
+ import { compileQuerySchema } from './query-keyword.js';
51
+ import { compileDollarDataSchema } from './dollar-data.js';
52
+ import { wrapUnevaluated } from './unevaluated.js';
53
+ import { createJsonPointer } from './traverse.js';
54
+
55
+ /**
56
+ * Compile $recursiveRef (draft 2019-09) and $dynamicRef (draft 2020-12).
57
+ * These keywords require runtime resolution based on dynamic scope.
58
+ *
59
+ * $recursiveRef: References the nearest parent schema with $recursiveAnchor: true
60
+ * $dynamicRef: References the nearest parent schema with matching $dynamicAnchor name
61
+ *
62
+ * @param {ValidationObject} schemaObj - The validation object
63
+ * @param {object} jsonSchema - The JSON schema
64
+ * @returns {Function|undefined} The compiled validator function
65
+ */
66
+ function compileDynamicRef(schemaObj, jsonSchema) {
67
+ // Check for $recursiveRef (draft 2019-09)
68
+ if (hasSchemaRecursiveRef(jsonSchema)) {
69
+ return compileRecursiveRef(schemaObj, jsonSchema);
70
+ }
71
+
72
+ // Check for $dynamicRef (draft 2020-12)
73
+ if (hasSchemaDynamicRef(jsonSchema)) {
74
+ return compileDynamicAnchorRef(schemaObj, jsonSchema);
75
+ }
76
+
77
+ return undefined;
78
+ }
79
+
80
+ /**
81
+ * Compile $recursiveRef which references the outermost $recursiveAnchor: true.
82
+ *
83
+ * Per JSON Schema 2019-09 spec:
84
+ * 1. The initial target is determined by resolving the reference as a URI reference
85
+ * against the current base URI (like $ref)
86
+ * 2. If the initial target has $recursiveAnchor: true, look up the dynamic scope
87
+ * for the outermost $recursiveAnchor and use that schema instead
88
+ * 3. Otherwise, use the initial target (like a normal $ref)
89
+ *
90
+ * @param {ValidationObject} schemaObj - The validation object
91
+ * @param {object} jsonSchema - The JSON schema containing $recursiveRef
92
+ * @returns {Function} The compiled validator function
93
+ */
94
+ function compileRecursiveRef(schemaObj, jsonSchema) {
95
+ const root = schemaObj.root;
96
+ const ref = jsonSchema.$recursiveRef;
97
+ const addError = schemaObj.createErrorHandler(ref, '$recursiveRef');
98
+
99
+ // $recursiveRef only supports "#" (the current document root)
100
+ if (ref !== '#') {
101
+ // For non-# refs, fall back to normal $ref behavior
102
+ return undefined;
103
+ }
104
+
105
+ // Get the base URI for resolving the reference
106
+ // This is the effective base URI of the schema containing $recursiveRef
107
+ // (accounts for $id of containing schemas)
108
+ const baseUri = schemaObj.baseUri;
109
+
110
+ // Resolve the reference to find the initial target URI
111
+ // For "#", this resolves against the baseUri
112
+ const { id: initialTargetUri } = createJsonPointer(ref, baseUri);
113
+
114
+ // Look up the initial target schema by its URI
115
+ // We do a direct lookup in the schemas map to avoid triggering compilation
116
+ // The initialTargetUri might have a trailing '#' which we need to handle
117
+ let initialTargetSchema = root.getSchemaByUri(initialTargetUri);
118
+ if (!initialTargetSchema && initialTargetUri.endsWith('#')) {
119
+ initialTargetSchema = root.getSchemaByUri(initialTargetUri.slice(0, -1));
120
+ }
121
+
122
+ // Check if the initial target has $recursiveAnchor: true
123
+ // If it does, we need to use the dynamic scope; otherwise, treat like normal $ref
124
+ const useDynamicScope = isObjectClass(initialTargetSchema) && initialTargetSchema.$recursiveAnchor === true;
125
+
126
+ return function validateRecursiveRef(data, dataPath, dataRoot) {
127
+ if (useDynamicScope) {
128
+ // The initial target has $recursiveAnchor: true
129
+ // Look up the dynamic scope for the outermost $recursiveAnchor
130
+ const outermostValidator = root.getOutermostDynamicAnchorValidator('');
131
+
132
+ if (outermostValidator) {
133
+ // Found an outermost $recursiveAnchor - use that validator
134
+ return outermostValidator(data, dataPath, dataRoot);
135
+ }
136
+ }
137
+
138
+ // Either no $recursiveAnchor on initial target, or no dynamic scope available
139
+ // Fall back to normal resolution like $ref
140
+ const targetObj = root.resolveObject(initialTargetUri, baseUri, jsonSchema);
141
+ if (targetObj) {
142
+ return targetObj.validate(data, dataPath, dataRoot);
143
+ }
144
+
145
+ return addError(data, dataPath);
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Compile $dynamicRef which references the nearest matching $dynamicAnchor.
151
+ *
152
+ * Per JSON Schema 2020-12 spec:
153
+ * 1. The initial target is determined by resolving the reference as a URI reference
154
+ * against the current base URI (like $ref)
155
+ * 2. If the initial target has $dynamicAnchor with matching name, look up the dynamic scope
156
+ * for the nearest $dynamicAnchor with that name and use that schema instead
157
+ * 3. Otherwise, use the initial target (like a normal $ref)
158
+ *
159
+ * @param {ValidationObject} schemaObj - The validation object
160
+ * @param {object} jsonSchema - The JSON schema containing $dynamicRef
161
+ * @returns {Function} The compiled validator function
162
+ */
163
+ function compileDynamicAnchorRef(schemaObj, jsonSchema) {
164
+ const root = schemaObj.root;
165
+ const ref = jsonSchema.$dynamicRef;
166
+ const addError = schemaObj.createErrorHandler(ref, '$dynamicRef');
167
+
168
+ // A $dynamicRef whose fragment is a JSON POINTER (not a plain-name anchor)
169
+ // behaves identically to $ref: no dynamic resolution takes place.
170
+ if (ref.startsWith('#') && ref.charAt(1) === '/') {
171
+ const baseUri = schemaObj.baseUri;
172
+ const { id: resolvedRef } = createJsonPointer(ref, baseUri);
173
+ return function validateDynamicRefPointer(data, dataPath, dataRoot) {
174
+ let targetObj;
175
+ try {
176
+ targetObj = root.resolveObject(resolvedRef, baseUri, { $ref: resolvedRef });
177
+ } catch (_e) {
178
+ return addError(data, dataPath);
179
+ }
180
+ if (targetObj) {
181
+ return targetObj.validate(data, dataPath, dataRoot);
182
+ }
183
+ return addError(data, dataPath);
184
+ };
185
+ }
186
+
187
+ // $dynamicRef is typically a fragment reference like "#name"
188
+ // For non-hash references, fall back to normal $ref behavior
189
+ // BUT we must defer resolution to validation time to avoid infinite recursion
190
+ // when the target schema also has $dynamicRef
191
+ if (!ref.startsWith('#')) {
192
+ // Non-fragment $dynamicRef - defer resolution to validation time
193
+ const baseUri = schemaObj.baseUri;
194
+ const resolvedPointer = createJsonPointer(ref, baseUri);
195
+ const resolvedRef = resolvedPointer.id;
196
+
197
+ // Look up the target schema at compile time
198
+ let targetSchema = root.getSchemaByUri(resolvedRef);
199
+ if (!targetSchema && resolvedRef.includes('#')) {
200
+ // Try without fragment
201
+ const [baseRef] = resolvedRef.split('#');
202
+ targetSchema = root.getSchemaByUri(baseRef);
203
+ }
204
+
205
+ if (targetSchema) {
206
+ // Return a validator that creates the target object at validation time
207
+ // This avoids infinite recursion during compilation
208
+ return function validateDynamicRefAsRef(data, dataPath, dataRoot) {
209
+ let targetObj = root.unresolvedObject(resolvedRef);
210
+ if (targetObj === null) {
211
+ targetObj = root.createObject(resolvedRef, targetSchema, baseUri);
212
+ }
213
+ if (targetObj) {
214
+ return targetObj.validate(data, dataPath, dataRoot);
215
+ }
216
+ return addError(data, dataPath);
217
+ };
218
+ }
219
+ return addError;
220
+ }
221
+
222
+ const anchorName = ref.slice(1); // Remove the "#" prefix
223
+
224
+ // Get the base URI for resolving the reference
225
+ const baseUri = schemaObj.baseUri;
226
+
227
+ // Resolve the reference to find the initial target URI
228
+ const { id: initialTargetUri } = createJsonPointer(ref, baseUri);
229
+
230
+ // Look up the initial target schema by its URI
231
+ let initialTargetSchema = root.getSchemaByUri(initialTargetUri);
232
+ if (!initialTargetSchema && initialTargetUri.endsWith('#')) {
233
+ initialTargetSchema = root.getSchemaByUri(initialTargetUri.slice(0, -1));
234
+ }
235
+
236
+ // Check if the initial target has matching $dynamicAnchor
237
+ // If it does, we need to use the dynamic scope; otherwise, treat like normal $ref
238
+ const hasDynamicAnchor = isObjectClass(initialTargetSchema) && initialTargetSchema.$dynamicAnchor === anchorName;
239
+
240
+ return function validateDynamicRef(data, dataPath, dataRoot) {
241
+ if (hasDynamicAnchor) {
242
+ // The initial target has matching $dynamicAnchor
243
+ // Look up the dynamic scope for the nearest $dynamicAnchor with this name
244
+ const dynamicValidator = root.getDynamicAnchorValidator(anchorName);
245
+
246
+ if (dynamicValidator) {
247
+ // Found a matching $dynamicAnchor in scope - use that validator
248
+ return dynamicValidator(data, dataPath, dataRoot);
249
+ }
250
+
251
+ // No dynamic scope available - use the initial target directly
252
+ // The initial target schema was already found at compile time (initialTargetSchema)
253
+ // Check if already compiled, otherwise create the validation object
254
+ let targetObj = root.unresolvedObject(initialTargetUri);
255
+ if (targetObj === null) {
256
+ // Not compiled yet - create it using the initial target schema we found at compile time
257
+ targetObj = root.createObject(initialTargetUri, initialTargetSchema, baseUri);
258
+ }
259
+ if (targetObj) {
260
+ const targetValidator = targetObj.validate;
261
+ // Register this schema's dynamic anchor for the duration of the validation
262
+ // This allows nested $dynamicRef to find this anchor
263
+ root.pushDynamicAnchorValidator(anchorName, targetValidator);
264
+ try {
265
+ return targetValidator(data, dataPath, dataRoot);
266
+ } finally {
267
+ root.popDynamicAnchorValidator(anchorName);
268
+ }
269
+ }
270
+ }
271
+
272
+ // Either no $dynamicAnchor on initial target, or no dynamic scope available
273
+ // Fall back to normal resolution like $ref
274
+ // For this case, we look up the schema directly and create a validation object
275
+ const targetSchema = initialTargetSchema || root.getSchemaByUri(initialTargetUri);
276
+ if (targetSchema) {
277
+ let targetObj = root.unresolvedObject(initialTargetUri);
278
+ if (targetObj === null) {
279
+ targetObj = root.createObject(initialTargetUri, targetSchema, baseUri);
280
+ }
281
+ if (targetObj) {
282
+ return targetObj.validate(data, dataPath, dataRoot);
283
+ }
284
+ }
285
+
286
+ return addError(data, dataPath);
287
+ };
288
+ }
289
+
290
+ function compileRequired(schemaObj, jsonSchema) {
291
+ // if required is not true, we have nothing.
292
+ const required = getBoolishType(jsonSchema.required);
293
+ if (required !== true) return undefined;
294
+
295
+ const addError = schemaObj.createErrorHandler(required, 'required');
296
+
297
+ // the compiled named function.
298
+ return function validateRequiredType(data, dataPath) {
299
+ return data === undefined
300
+ ? addError(data, dataPath)
301
+ : true;
302
+ };
303
+ }
304
+
305
+ function compileTypeSimple(schemaObj, jsonSchema) {
306
+ const type = getStringType(jsonSchema.type);
307
+ if (type == null) return undefined;
308
+
309
+ const isDataType = createIsSchemaTypeHandler(type);
310
+ if (!isDataType) throw new Error(`The explicit schema type '${type}' is unknown. (TODO: add trace)`);
311
+
312
+ const addError = schemaObj.createErrorHandler(type, 'type');
313
+
314
+ return function validateTypeSimple(data, dataPath) {
315
+ return isDataType(data)
316
+ ? true
317
+ : addError(data, dataPath);
318
+ };
319
+ }
320
+
321
+ function compileTypeArray(schemaObj, jsonSchema) {
322
+ const schemaTypes = getUniqueArray(jsonSchema.type);
323
+ if (schemaTypes == null) return undefined;
324
+ if (schemaTypes.length === 0)
325
+ throw new Error('The schema type property can not be an empty array.');
326
+
327
+ // collect all testable data types
328
+ const types = [];
329
+ const names = [];
330
+ for (let i = 0; i < schemaTypes.length; ++i) {
331
+ const type = schemaTypes[i];
332
+ const callback = createIsSchemaTypeHandler(type);
333
+ if (!callback)
334
+ throw new Error(`The explicit schema type '${type}' of '${types} is unknown. (TODO: add trace)`);
335
+
336
+ types.push(callback);
337
+ names.push(type);
338
+ }
339
+
340
+ const addError = schemaObj.createErrorHandler(names, 'type');
341
+
342
+ // if one has been found create a validator
343
+ if (types.length === 1) {
344
+ const one = types[0];
345
+ return function validateSingleType(data, dataPath) {
346
+ return one(data)
347
+ ? true
348
+ : addError(data, dataPath);
349
+ };
350
+ }
351
+ else if (types.length === 2) {
352
+ const one = types[0];
353
+ const two = types[1];
354
+ return function validateDoubleTypes(data, dataPath) {
355
+ return one(data) || two(data)
356
+ ? true
357
+ : addError(data, dataPath);
358
+ };
359
+ }
360
+ else if (types.length === 3) {
361
+ const one = types[0];
362
+ const two = types[1];
363
+ const three = types[2];
364
+ return function validateTripleTypes(data, dataPath) {
365
+ return one(data) || two(data) || three(data)
366
+ ? true
367
+ : addError(data, dataPath);
368
+ };
369
+ }
370
+ else {
371
+ return function validateAllTypes(data, dataPath) {
372
+ for (let i = 0; i < types.length; ++i) {
373
+ if (types[i](data) === true) return true;
374
+ }
375
+ return addError(data, dataPath);
376
+ };
377
+ }
378
+ }
379
+
380
+ function compileTypeBasic(schemaObj, jsonSchema) {
381
+ const validator = compileTypeSimple(schemaObj, jsonSchema)
382
+ || compileTypeArray(schemaObj, jsonSchema);
383
+
384
+ const nullable = getBoolishType(jsonSchema.nullable);
385
+ if (validator == null) {
386
+ if (nullable !== false) return undefined;
387
+
388
+ const addError = schemaObj.createErrorHandler(nullable, 'nullable');
389
+
390
+ return function validateNotIsNull(data, dataPath) {
391
+ return data === undefined
392
+ || data !== null
393
+ || addError(data, dataPath);
394
+ };
395
+ }
396
+
397
+ if (nullable === true) {
398
+ return function validateNullableType(data, dataPath) {
399
+ return data === undefined
400
+ || data === null
401
+ || validator(data, dataPath);
402
+ };
403
+ }
404
+
405
+ return function validateType(data, dataPath) {
406
+ return data === undefined
407
+ || validator(data, dataPath);
408
+ };
409
+ }
410
+
411
+ export function compileSchemaObject(schemaObj, jsonSchema) {
412
+ if (jsonSchema === true) return trueThat;
413
+ if (jsonSchema === false) {
414
+ const addError = schemaObj.createErrorHandler(false, 'false schema');
415
+ return function validateFalseSchema(data, dataPath) {
416
+ return addError(data, dataPath);
417
+ };
418
+ }
419
+ if (!isObjectType(jsonSchema))
420
+ throw new Error('JSON Schema MUST be a boolean or Object Type');
421
+
422
+ const keys = Object.keys(jsonSchema);
423
+
424
+ // 'errorMessage' is report-time metadata: compile its spec once and
425
+ // register it on the root - NO validator closure is emitted (the keyword
426
+ // contributes zero validation-time work), and the key is excluded from
427
+ // the single-keyword counts so it cannot knock a node off the fast
428
+ // paths below.
429
+ let keyCount = keys.length;
430
+ if (jsonSchema.errorMessage !== undefined) {
431
+ schemaObj.root.registerErrorMessage(
432
+ schemaObj.path,
433
+ compileErrorMessageSpec(jsonSchema.errorMessage, schemaObj.path));
434
+ keyCount -= 1;
435
+ }
436
+
437
+ if (keys.length === 0)
438
+ return trueThat;
439
+
440
+ // In draft 7 and earlier, $ref completely replaces the schema
441
+ // and all sibling keywords must be ignored. In draft 2019-09+,
442
+ // $ref is just another keyword that can have siblings.
443
+ // We need to check the actual behavior based on schema context.
444
+ // If schema has ONLY $ref (and meta keywords), use the ref-only path.
445
+ // If schema has $ref with validation siblings, process them together (2019-09+ only).
446
+ // The resource's own declared draft decides $ref-sibling behavior; see the
447
+ // note in ValidationObject.compileValidator.
448
+ const draftVersion = schemaObj.declaredDraft ?? schemaObj.options.draftVersion ?? 7;
449
+ // When compiling the sibling keywords of a $ref schema, the unevaluated*
450
+ // wrapper is applied by ValidationObject.compileValidator around the
451
+ // combined (ref + siblings) validator instead of here, so that the
452
+ // $ref target's annotations are visible to the unevaluated* check.
453
+ let refWithSiblings = false;
454
+ if (hasSchemaRef(jsonSchema) && !hasSchemaRecursiveRef(jsonSchema)) {
455
+ // Check if there are any validation-related sibling keywords
456
+ // In draft 2019-09+, if there are validation siblings, we process
457
+ // them together. Membership-only (order-insensitive): the shared
458
+ // constraint groups plus the applicators and extras.
459
+ const validationKeywords = ['type', 'const', 'enum',
460
+ ...NUMERIC_CONSTRAINTS, ...STRING_CONSTRAINTS,
461
+ ...ARRAY_CONSTRAINTS, 'maxContains', 'minContains',
462
+ ...OBJECT_CONSTRAINTS, 'required',
463
+ 'dependentRequired', 'properties', 'patternProperties', 'additionalProperties', 'items',
464
+ 'prefixItems', 'additionalItems', 'contains', 'allOf', 'anyOf', 'oneOf', 'not', 'if',
465
+ 'then', 'else', 'propertyNames', 'contentEncoding', 'contentMediaType',
466
+ 'unevaluatedProperties', 'unevaluatedItems', '$query'];
467
+ const hasValidationSiblings = keys.some(k => validationKeywords.includes(k));
468
+ // In draft 7 and earlier, $ref always overrides siblings regardless
469
+ // In draft 2019-09+, $ref can have validation siblings
470
+ if (!hasValidationSiblings || draftVersion < 2019) {
471
+ return undefined;
472
+ }
473
+ // Otherwise, continue to process siblings alongside $ref (2019-09+ only)
474
+ refWithSiblings = true;
475
+ }
476
+
477
+ // When the metaschema's $vocabulary omits the validation vocabulary,
478
+ // keywords like type/enum/minimum/minLength assert nothing.
479
+ const vocabValidation = schemaObj.options.vocabValidation !== false;
480
+
481
+ // Fast paths for common simple schema patterns
482
+ // These inline the validation to reduce function call overhead
483
+
484
+ // Fast path: type-only schema (most common case: {"type": "string"})
485
+ if (vocabValidation && keyCount === 1 && jsonSchema.type !== undefined) {
486
+ const type = jsonSchema.type;
487
+ // Only handle single type strings here (not arrays of types)
488
+ if (typeof type === 'string') {
489
+ const addError = schemaObj.createErrorHandler(type, 'type');
490
+
491
+ switch (type) {
492
+ case 'string':
493
+ return function validateTypeStringOnly(data, dataPath) {
494
+ return data === undefined || typeof data === 'string' || addError(data, dataPath);
495
+ };
496
+ case 'number':
497
+ return function validateTypeNumberOnly(data, dataPath) {
498
+ return data === undefined || (typeof data === 'number' && !isNaN(data)) || addError(data, dataPath);
499
+ };
500
+ case 'integer':
501
+ return function validateTypeIntegerOnly(data, dataPath) {
502
+ return data === undefined || Number.isInteger(data) || addError(data, dataPath);
503
+ };
504
+ case 'boolean':
505
+ return function validateTypeBooleanOnly(data, dataPath) {
506
+ return data === undefined || typeof data === 'boolean' || addError(data, dataPath);
507
+ };
508
+ case 'array':
509
+ return function validateTypeArrayOnly(data, dataPath) {
510
+ return data === undefined || Array.isArray(data) || addError(data, dataPath);
511
+ };
512
+ case 'object':
513
+ return function validateTypeObjectOnly(data, dataPath) {
514
+ return data === undefined || (typeof data === 'object' && data !== null && !Array.isArray(data))
515
+ || addError(data, dataPath);
516
+ };
517
+ case 'null':
518
+ return function validateTypeNullOnly(data, dataPath) {
519
+ return data === undefined || data === null || addError(data, dataPath);
520
+ };
521
+ }
522
+ }
523
+ }
524
+
525
+ // Fast path: required-only schema (common case: {"required": ["foo", "bar"]})
526
+ if (vocabValidation && keyCount === 1 && jsonSchema.required !== undefined) {
527
+ const required = jsonSchema.required;
528
+ // Non-string entries (invalid schemas) can never match a data key;
529
+ // they stay on the generic Object.keys path.
530
+ if (Array.isArray(required) && required.length > 0
531
+ && required.every(key => typeof key === 'string')) {
532
+ const addError = schemaObj.createErrorHandler(required, ['required']);
533
+ const rlen = required.length;
534
+
535
+ if (schemaObj.options.skipErrors) {
536
+ return function validateRequiredOnly(data, dataPath) {
537
+ // Required only applies to objects, not arrays or primitives
538
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return true;
539
+ for (let i = 0; i < rlen; i++) {
540
+ if (!Object.hasOwn(data, required[i])) {
541
+ return addError(required[i], data, dataPath);
542
+ }
543
+ }
544
+ return true;
545
+ };
546
+ }
547
+
548
+ // Every absent property is its own fault to report; the fast path must
549
+ // not be the reason a caller only learns about the first one.
550
+ return function validateRequiredOnlyAll(data, dataPath) {
551
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return true;
552
+ let valid = true;
553
+ for (let i = 0; i < rlen; i++) {
554
+ if (!Object.hasOwn(data, required[i]))
555
+ valid = addError(required[i], data, dataPath) && valid;
556
+ }
557
+ return valid;
558
+ };
559
+ }
560
+ }
561
+
562
+ // Fast path: minLength-only schema (common case: {"minLength": 2})
563
+ // This avoids the overhead of compileStringBasic for simple cases
564
+ if (vocabValidation && keyCount === 1 && jsonSchema.minLength !== undefined) {
565
+ const min = jsonSchema.minLength;
566
+ if (typeof min === 'number' && min > 0 && Number.isFinite(min)) {
567
+ const addError = schemaObj.createErrorHandler(min, 'minLength');
568
+ const useGrapheme = schemaObj.options.useGrapheme;
569
+
570
+ if (!useGrapheme) {
571
+ // Simple byte counting
572
+ return function validateMinLengthOnly(data, dataPath) {
573
+ if (typeof data !== 'string') return true;
574
+ return data.length >= min || addError(data.length, dataPath);
575
+ };
576
+ } else {
577
+ // Grapheme counting - use getStringLength
578
+ return function validateMinLengthGrapheme(data, dataPath) {
579
+ if (typeof data !== 'string') return true;
580
+ const len = getStringLength(data, true);
581
+ return len >= min || addError(len, dataPath);
582
+ };
583
+ }
584
+ }
585
+ }
586
+
587
+ // Fast path: maxLength-only schema (common case: {"maxLength": 10})
588
+ if (vocabValidation && keyCount === 1 && jsonSchema.maxLength !== undefined) {
589
+ const max = jsonSchema.maxLength;
590
+ if (typeof max === 'number' && max >= 0 && Number.isFinite(max)) {
591
+ const addError = schemaObj.createErrorHandler(max, 'maxLength');
592
+ const useGrapheme = schemaObj.options.useGrapheme;
593
+
594
+ if (!useGrapheme) {
595
+ return function validateMaxLengthOnly(data, dataPath) {
596
+ if (typeof data !== 'string') return true;
597
+ return data.length <= max || addError(data.length, dataPath);
598
+ };
599
+ } else {
600
+ // Grapheme counting - use getStringLength
601
+ return function validateMaxLengthGrapheme(data, dataPath) {
602
+ if (typeof data !== 'string') return true;
603
+ const len = getStringLength(data, true);
604
+ return len <= max || addError(len, dataPath);
605
+ };
606
+ }
607
+ }
608
+ }
609
+
610
+ const validators = [];
611
+ if (vocabValidation) {
612
+ addFunctionToArray(validators, compileRequired(schemaObj, jsonSchema));
613
+ addFunctionToArray(validators, compileTypeBasic(schemaObj, jsonSchema));
614
+ addFunctionToArray(validators, compileEnumBasic(schemaObj, jsonSchema));
615
+
616
+ // Compile $data-aware validators for keywords with $data references
617
+ // This handles cases like: { "maximum": { "$data": "1/larger" } }
618
+ const dollarDataValidator = compileDollarDataSchema(schemaObj, jsonSchema);
619
+ addFunctionToArray(validators, dollarDataValidator);
620
+
621
+ addFunctionToArray(validators, compileNumberBasic(schemaObj, jsonSchema));
622
+ addFunctionToArray(validators, compileBigIntBasic(schemaObj, jsonSchema));
623
+ addFunctionToArray(validators, compileStringBasic(schemaObj, jsonSchema));
624
+ }
625
+ addFunctionToArray(validators, compileFormatBasic(schemaObj, jsonSchema));
626
+ addFunctionToArray(validators, compileContentSchema(schemaObj, jsonSchema));
627
+
628
+ addFunctionToArray(validators, compileArraySchema(schemaObj, jsonSchema));
629
+ addFunctionToArray(validators, compileObjectSchema(schemaObj, jsonSchema));
630
+
631
+ addFunctionToArray(validators, compileCombineSchema(schemaObj, jsonSchema));
632
+ addFunctionToArray(validators, compileConditionSchema(schemaObj, jsonSchema));
633
+ addFunctionToArray(validators, compileDataSchema(schemaObj, jsonSchema));
634
+ addFunctionToArray(validators, compileQuerySchema(schemaObj, jsonSchema));
635
+
636
+ // Compile $recursiveRef (draft 2019-09) and $dynamicRef (draft 2020-12)
637
+ addFunctionToArray(validators, compileDynamicRef(schemaObj, jsonSchema));
638
+
639
+ // The unevaluated* keywords run last, after every other keyword and
640
+ // in-place applicator has produced its annotations. For $ref siblings
641
+ // the wrapper is applied by the caller (see refWithSiblings above).
642
+ const finalize = refWithSiblings
643
+ ? (validator) => validator
644
+ : (validator) => wrapUnevaluated(schemaObj, jsonSchema, validator);
645
+
646
+ // same as empty schema
647
+ if (validators.length === 0)
648
+ return finalize(trueThat);
649
+
650
+ if (validators.length === 1)
651
+ return finalize(validators[0]);
652
+
653
+ // These are the node's KEYWORD GROUPS (type, string, number, object,
654
+ // array, combine, ...) and they are independent of one another: each
655
+ // re-guards the data type it applies to, so continuing past a failed group
656
+ // is safe. Short-circuiting them is a boolean-mode optimization; when
657
+ // errors are recorded it lets one group's failure hide every other group's.
658
+ if (!schemaObj.options.skipErrors)
659
+ return finalize(combineIndependent(validators));
660
+
661
+ if (validators.length === 2) {
662
+ const first = validators[0];
663
+ const second = validators[1];
664
+ return finalize(function validateDoubleSchemaObject(data, dataPath, dataRoot) {
665
+ return first(data, dataPath, dataRoot)
666
+ && second(data, dataPath, dataRoot);
667
+ });
668
+ }
669
+
670
+ if (validators.length === 3) {
671
+ const first = validators[0];
672
+ const second = validators[1];
673
+ const thirth = validators[2];
674
+ return finalize(function validateTripleSchemaObject(data, dataPath, dataRoot) {
675
+ return first(data, dataPath, dataRoot)
676
+ && second(data, dataPath, dataRoot)
677
+ && thirth(data, dataPath, dataRoot);
678
+ });
679
+ }
680
+
681
+ return finalize(function validateAllSchemaObject(data, dataPath, dataRoot) {
682
+ for (let i = 0; i < validators.length; ++i) {
683
+ const validator = validators[i];
684
+ if (validator(data, dataPath, dataRoot) === false) {
685
+ return false;
686
+ }
687
+ }
688
+ return true;
689
+ });
690
+ }