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