@jarenjs/validate 0.9.2 → 0.34.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.
- package/ARCHITECTURE.md +81 -17
- package/README.md +462 -5
- package/dist/types/dollar-data.d.ts +0 -9
- package/dist/types/index.d.ts +167 -69
- package/dist/types/messages.d.ts +142 -0
- package/dist/types/normalize.d.ts +107 -0
- package/dist/types/tools.d.ts +58 -0
- package/docs/ERROR-MESSAGES.md +251 -0
- package/package.json +9 -4
- package/src/array.js +68 -23
- package/src/bigint.js +18 -7
- package/src/combine.js +61 -11
- package/src/condition.js +34 -14
- package/src/content.js +3 -3
- package/src/data.js +11 -387
- package/src/dollar-data.js +55 -472
- package/src/enum.js +0 -1
- package/src/format.js +46 -4
- package/src/index.js +280 -238
- package/src/messages.js +497 -0
- package/src/normalize.js +585 -0
- package/src/number.js +19 -9
- package/src/object.js +126 -33
- package/src/query.js +28 -2
- package/src/schema.js +72 -27
- package/src/string.js +18 -6
- package/src/tools.js +192 -0
- package/src/traverse.js +13 -4
- package/src/unevaluated.js +27 -5
package/src/object.js
CHANGED
|
@@ -62,6 +62,9 @@ function compileRequiredProperties(schemaObj, jsonSchema) {
|
|
|
62
62
|
/** @type {function(string, any, string):boolean} */
|
|
63
63
|
// Use array key to get keyed error handler: addKeyedError(dataKey, data, ...meta)
|
|
64
64
|
const addError = schemaObj.createErrorHandler(required, ['required']);
|
|
65
|
+
// When errors are recorded, every missing property must produce one; only
|
|
66
|
+
// the boolean-answer path may stop at the first.
|
|
67
|
+
const stopAtFirst = schemaObj.root.options.skipErrors;
|
|
65
68
|
return function validateRequiredProperties(data = {}, dataKeys = [], dataPath = '') {
|
|
66
69
|
if (!(dataKeys.length > 0))
|
|
67
70
|
return false;
|
|
@@ -70,8 +73,10 @@ function compileRequiredProperties(schemaObj, jsonSchema) {
|
|
|
70
73
|
for (let i = 0; i < rlength; ++i) {
|
|
71
74
|
const key = required[i];
|
|
72
75
|
const idx = dataKeys.indexOf(key);
|
|
73
|
-
if (idx === -1)
|
|
74
|
-
valid
|
|
76
|
+
if (idx === -1) {
|
|
77
|
+
valid = addError(key, data, dataPath) && valid;
|
|
78
|
+
if (stopAtFirst) break;
|
|
79
|
+
}
|
|
75
80
|
}
|
|
76
81
|
return valid;
|
|
77
82
|
};
|
|
@@ -84,8 +89,10 @@ function compilePropertyNames(schemaObj, jsonSchema) {
|
|
|
84
89
|
if (propNames == null) return undefined;
|
|
85
90
|
|
|
86
91
|
const propertyNamesValidator = schemaObj.createValidator(propNames, 'propertyNames');
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
// The property NAME is the validated data; the object's data path is
|
|
93
|
+
// threaded through so name failures report a usable instancePath.
|
|
94
|
+
return function validatePropertyNames(dataKey, dataPath) {
|
|
95
|
+
return propertyNamesValidator(dataKey, dataPath);
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
98
|
|
|
@@ -185,7 +192,7 @@ function compileAdditionalProperties(schemaObj, jsonSchema) {
|
|
|
185
192
|
const addError = schemaObj.createErrorHandler(false, ['additionalProperties']);
|
|
186
193
|
|
|
187
194
|
return function validateNoAdditionalProperties(data, dataPath, dataRoot, dataKey) {
|
|
188
|
-
return addError(dataKey, data);
|
|
195
|
+
return addError(dataKey, data, dataPath);
|
|
189
196
|
};
|
|
190
197
|
}
|
|
191
198
|
|
|
@@ -216,13 +223,15 @@ function compileDependentRequired(schemaObj, jsonSchema) {
|
|
|
216
223
|
if (Object.keys(dependentRequired).length === 0)
|
|
217
224
|
return undefined;
|
|
218
225
|
|
|
219
|
-
|
|
226
|
+
// Keyed handler: addError(dataKey, data, dataPath) - dataKey names the
|
|
227
|
+
// triggering property, rest[0] is the instance data path.
|
|
228
|
+
const addError = schemaObj.createErrorHandler(false, ['dependentRequired']);
|
|
220
229
|
|
|
221
230
|
return function validateDependentRequiredItem(data, dataPath, dataRoot, dataKey) {
|
|
222
231
|
if (dataKey in dependentRequired) {
|
|
223
232
|
const required = dependentRequired[dataKey];
|
|
224
233
|
return includesAll(Object.keys(data), required)
|
|
225
|
-
|| addError(
|
|
234
|
+
|| addError(dataKey, data, dataPath);
|
|
226
235
|
}
|
|
227
236
|
return true;
|
|
228
237
|
};
|
|
@@ -320,7 +329,7 @@ function compileDependencies(schemaObj, jsonSchema) {
|
|
|
320
329
|
const dataKeys = Object.keys(data);
|
|
321
330
|
for (let i = 0; i < rlen; i++) {
|
|
322
331
|
if (!dataKeys.includes(required[i])) {
|
|
323
|
-
return addError(
|
|
332
|
+
return addError(dataKey, data, dataPath);
|
|
324
333
|
}
|
|
325
334
|
}
|
|
326
335
|
}
|
|
@@ -350,7 +359,7 @@ function compileDependencies(schemaObj, jsonSchema) {
|
|
|
350
359
|
if (reqDep != null) {
|
|
351
360
|
const { required, addError } = reqDep;
|
|
352
361
|
return includesAll(Object.keys(data), required)
|
|
353
|
-
|| addError(
|
|
362
|
+
|| addError(dataKey, data, dataPath);
|
|
354
363
|
}
|
|
355
364
|
return true;
|
|
356
365
|
};
|
|
@@ -382,6 +391,9 @@ export function compileObjectPrimitives(schemaObj, jsonSchema) {
|
|
|
382
391
|
const hasMin = min > 0;
|
|
383
392
|
const hasMax = max != null && max >= 0;
|
|
384
393
|
const hasRequired = required != null && required.length > 0;
|
|
394
|
+
// When errors are recorded, every missing required property must produce
|
|
395
|
+
// one; only the boolean-answer path may stop at the first.
|
|
396
|
+
const stopAtFirst = schemaObj.root.options.skipErrors;
|
|
385
397
|
|
|
386
398
|
// Pre-bind error handlers outside the returned function
|
|
387
399
|
if (hasMin && !hasMax && !hasRequired) {
|
|
@@ -414,6 +426,25 @@ export function compileObjectPrimitives(schemaObj, jsonSchema) {
|
|
|
414
426
|
if (!hasMin && !hasMax && hasRequired) {
|
|
415
427
|
const rlength = required.length;
|
|
416
428
|
const addError = schemaObj.createErrorHandler(required, ['required']);
|
|
429
|
+
// Non-string entries (invalid schemas) can never match a data key;
|
|
430
|
+
// they stay on the Object.keys path below.
|
|
431
|
+
if (required.every(key => typeof key === 'string')) {
|
|
432
|
+
return function validateRequiredHasOwn(data, dataPath, _dataRoot, _dataKeys) {
|
|
433
|
+
// Required properties only apply to objects, not arrays or other types
|
|
434
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
let valid = true;
|
|
438
|
+
for (let i = 0; i < rlength; ++i) {
|
|
439
|
+
const key = required[i];
|
|
440
|
+
if (!Object.hasOwn(data, key)) {
|
|
441
|
+
valid = addError(key, data, dataPath) && valid;
|
|
442
|
+
if (stopAtFirst) break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return valid;
|
|
446
|
+
};
|
|
447
|
+
}
|
|
417
448
|
return function validateRequiredOnly(data, dataPath, dataRoot, dataKeys) {
|
|
418
449
|
// Required properties only apply to objects, not arrays or other types
|
|
419
450
|
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
@@ -423,8 +454,10 @@ export function compileObjectPrimitives(schemaObj, jsonSchema) {
|
|
|
423
454
|
let valid = true;
|
|
424
455
|
for (let i = 0; i < rlength; ++i) {
|
|
425
456
|
const key = required[i];
|
|
426
|
-
if (keys.indexOf(key) === -1)
|
|
427
|
-
valid
|
|
457
|
+
if (keys.indexOf(key) === -1) {
|
|
458
|
+
valid = addError(key, data, dataPath) && valid;
|
|
459
|
+
if (stopAtFirst) break;
|
|
460
|
+
}
|
|
428
461
|
}
|
|
429
462
|
return valid;
|
|
430
463
|
};
|
|
@@ -441,15 +474,19 @@ export function compileObjectPrimitives(schemaObj, jsonSchema) {
|
|
|
441
474
|
}
|
|
442
475
|
const keys = dataKeys || Object.keys(data);
|
|
443
476
|
const len = keys.length;
|
|
444
|
-
|
|
445
|
-
|
|
477
|
+
// minProperties and required are independent; a short count says
|
|
478
|
+
// nothing about WHICH members are missing, which is the useful half.
|
|
479
|
+
let sizeOk = len >= min || addMinError(len, dataPath);
|
|
480
|
+
if (stopAtFirst && !sizeOk) return false;
|
|
446
481
|
let valid = true;
|
|
447
482
|
for (let i = 0; i < rlength; ++i) {
|
|
448
483
|
const key = required[i];
|
|
449
|
-
if (keys.indexOf(key) === -1)
|
|
450
|
-
valid
|
|
484
|
+
if (keys.indexOf(key) === -1) {
|
|
485
|
+
valid = addReqError(key, data, dataPath) && valid;
|
|
486
|
+
if (stopAtFirst) break;
|
|
487
|
+
}
|
|
451
488
|
}
|
|
452
|
-
return valid;
|
|
489
|
+
return sizeOk && valid;
|
|
453
490
|
};
|
|
454
491
|
}
|
|
455
492
|
|
|
@@ -464,15 +501,18 @@ export function compileObjectPrimitives(schemaObj, jsonSchema) {
|
|
|
464
501
|
}
|
|
465
502
|
const keys = dataKeys || Object.keys(data);
|
|
466
503
|
const len = keys.length;
|
|
467
|
-
|
|
468
|
-
|
|
504
|
+
// maxProperties and required are independent (see the min case).
|
|
505
|
+
let sizeOk = len <= max || addMaxError(len, dataPath);
|
|
506
|
+
if (stopAtFirst && !sizeOk) return false;
|
|
469
507
|
let valid = true;
|
|
470
508
|
for (let i = 0; i < rlength; ++i) {
|
|
471
509
|
const key = required[i];
|
|
472
|
-
if (keys.indexOf(key) === -1)
|
|
473
|
-
valid
|
|
510
|
+
if (keys.indexOf(key) === -1) {
|
|
511
|
+
valid = addReqError(key, data, dataPath) && valid;
|
|
512
|
+
if (stopAtFirst) break;
|
|
513
|
+
}
|
|
474
514
|
}
|
|
475
|
-
return valid;
|
|
515
|
+
return sizeOk && valid;
|
|
476
516
|
};
|
|
477
517
|
}
|
|
478
518
|
|
|
@@ -484,9 +524,16 @@ export function compileObjectPrimitives(schemaObj, jsonSchema) {
|
|
|
484
524
|
return function validateObjectPrimitives(data, dataPath, dataRoot, dataKeys) {
|
|
485
525
|
const keys = dataKeys || Object.keys(data);
|
|
486
526
|
const len = keys.length;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
527
|
+
if (stopAtFirst) {
|
|
528
|
+
return isMinProperties(len, dataPath)
|
|
529
|
+
&& isMaxProperties(len, dataPath)
|
|
530
|
+
&& hasRequiredProperties(data, keys, dataPath);
|
|
531
|
+
}
|
|
532
|
+
// min/max/required are three independent assertions about the same
|
|
533
|
+
// object; each is the only thing that can report its own fault.
|
|
534
|
+
let valid = isMinProperties(len, dataPath);
|
|
535
|
+
valid = isMaxProperties(len, dataPath) && valid;
|
|
536
|
+
return hasRequiredProperties(data, keys, dataPath) && valid;
|
|
490
537
|
};
|
|
491
538
|
}
|
|
492
539
|
|
|
@@ -521,7 +568,7 @@ function compileObjectProperty(schemaObj, jsonSchema) {
|
|
|
521
568
|
// Build the child dataPath by appending the property key
|
|
522
569
|
const newPath = dataPath ? `${dataPath}/${dataKey}` : `/${dataKey}`;
|
|
523
570
|
|
|
524
|
-
result.addValid(validateName(dataKey))
|
|
571
|
+
result.addValid(validateName(dataKey, dataPath))
|
|
525
572
|
.addResult(validateProperty(data, newPath, dataRoot, dataKey))
|
|
526
573
|
.addResult(validatePattern(data, newPath, dataRoot, dataKey))
|
|
527
574
|
.addValid(validateDepRequired(data, newPath, dataRoot, dataKey))
|
|
@@ -584,7 +631,7 @@ function compileObjectChildrenFast(schemaObj, jsonSchema) {
|
|
|
584
631
|
const len = dataKeys.length;
|
|
585
632
|
for (let i = 0; i < len; ++i) {
|
|
586
633
|
const dataKey = dataKeys[i];
|
|
587
|
-
if (validateName != null && validateName(dataKey) === false)
|
|
634
|
+
if (validateName != null && validateName(dataKey, dataPath) === false)
|
|
588
635
|
return false;
|
|
589
636
|
|
|
590
637
|
let matched = false;
|
|
@@ -675,9 +722,10 @@ export function compileObjectSchema(schemaObj, jsonSchema) {
|
|
|
675
722
|
if (isOfSchemaType(jsonSchema, 'map'))
|
|
676
723
|
return undefined;
|
|
677
724
|
|
|
678
|
-
// Fast path: properties-only schema in skipErrors mode.
|
|
679
|
-
// (fixed) schema keys with direct property access instead of
|
|
680
|
-
// Object.keys(data) and doing a map lookup per data key
|
|
725
|
+
// Fast path: properties(+required)-only schema in skipErrors mode.
|
|
726
|
+
// Iterate the (fixed) schema keys with direct property access instead of
|
|
727
|
+
// allocating Object.keys(data) and doing a map lookup per data key;
|
|
728
|
+
// required membership is a per-key Object.hasOwn probe.
|
|
681
729
|
if (schemaObj.options.skipErrors
|
|
682
730
|
&& jsonSchema.patternProperties == null
|
|
683
731
|
&& jsonSchema.additionalProperties == null
|
|
@@ -687,12 +735,21 @@ export function compileObjectSchema(schemaObj, jsonSchema) {
|
|
|
687
735
|
&& jsonSchema.dependentRequired == null
|
|
688
736
|
&& jsonSchema.minProperties == null
|
|
689
737
|
&& jsonSchema.maxProperties == null
|
|
690
|
-
&& jsonSchema.required == null
|
|
738
|
+
&& (jsonSchema.required == null
|
|
739
|
+
|| (isArrayClass(jsonSchema.required)
|
|
740
|
+
&& jsonSchema.required.every(key => typeof key === 'string')))
|
|
691
741
|
&& getObjectType(jsonSchema.properties) != null) {
|
|
692
742
|
const propsMap = buildPropertyValidators(schemaObj, jsonSchema);
|
|
693
743
|
if (propsMap == null)
|
|
694
744
|
return undefined;
|
|
695
745
|
|
|
746
|
+
// required belongs to the validation vocabulary; assert nothing when
|
|
747
|
+
// the metaschema disables it.
|
|
748
|
+
const requiredKeys = schemaObj.options.vocabValidation !== false
|
|
749
|
+
? getArrayClassMinItems(jsonSchema.required, 1) || null
|
|
750
|
+
: null;
|
|
751
|
+
const requiredCount = requiredKeys === null ? 0 : requiredKeys.length;
|
|
752
|
+
|
|
696
753
|
const propKeys = Array.from(propsMap.keys());
|
|
697
754
|
const propValidators = Array.from(propsMap.values());
|
|
698
755
|
const propCount = propKeys.length;
|
|
@@ -706,6 +763,10 @@ export function compileObjectSchema(schemaObj, jsonSchema) {
|
|
|
706
763
|
const extendPaths = root.usesDollarData;
|
|
707
764
|
return function validateObjectPropertiesOnlyTracked(data, dataPath, dataRoot) {
|
|
708
765
|
if (!isObjectType(data)) return true;
|
|
766
|
+
for (let i = 0; i < requiredCount; ++i) {
|
|
767
|
+
if (!Object.hasOwn(data, requiredKeys[i]))
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
709
770
|
for (let i = 0; i < propCount; ++i) {
|
|
710
771
|
const key = propKeys[i];
|
|
711
772
|
// Object.hasOwn: avoid picking up inherited members like toString
|
|
@@ -722,6 +783,10 @@ export function compileObjectSchema(schemaObj, jsonSchema) {
|
|
|
722
783
|
|
|
723
784
|
return function validateObjectPropertiesOnly(data, dataPath, dataRoot) {
|
|
724
785
|
if (!isObjectType(data)) return true;
|
|
786
|
+
for (let i = 0; i < requiredCount; ++i) {
|
|
787
|
+
if (!Object.hasOwn(data, requiredKeys[i]))
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
725
790
|
for (let i = 0; i < propCount; ++i) {
|
|
726
791
|
const key = propKeys[i];
|
|
727
792
|
// Object.hasOwn: avoid picking up inherited members like toString
|
|
@@ -743,11 +808,39 @@ export function compileObjectSchema(schemaObj, jsonSchema) {
|
|
|
743
808
|
const validatePrimitives = objectPrimitives || trueThat;
|
|
744
809
|
const validateChildren = objectChildren || trueThat;
|
|
745
810
|
|
|
746
|
-
|
|
811
|
+
// Without child validators the data keys are only consumed by the
|
|
812
|
+
// min/max length checks, which compute them on demand; skip the
|
|
813
|
+
// per-validation Object.keys allocation.
|
|
814
|
+
if (objectChildren == null) {
|
|
815
|
+
return function validateObjectPrimitivesSchema(data, dataPath, dataRoot) {
|
|
816
|
+
if (isObjectType(data)) {
|
|
817
|
+
return validatePrimitives(data, dataPath, dataRoot, undefined);
|
|
818
|
+
}
|
|
819
|
+
return true;
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (schemaObj.options.skipErrors) {
|
|
824
|
+
return function validateObjectSchema(data, dataPath, dataRoot) {
|
|
825
|
+
if (isObjectType(data)) {
|
|
826
|
+
const dataKeys = Object.keys(data);
|
|
827
|
+
return validatePrimitives(data, dataPath, dataRoot, dataKeys)
|
|
828
|
+
&& validateChildren(data, dataPath, dataRoot, dataKeys);
|
|
829
|
+
}
|
|
830
|
+
return true;
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// `required`/`minProperties` and the per-property schemas are independent:
|
|
835
|
+
// the child walk re-derives everything it needs from the data and cannot
|
|
836
|
+
// fault on a missing key. Stopping after a missing `required` would report
|
|
837
|
+
// the absent property and hide every fault in the properties that ARE
|
|
838
|
+
// present, which is the difference between one issue and a usable list.
|
|
839
|
+
return function validateObjectSchemaAll(data, dataPath, dataRoot) {
|
|
747
840
|
if (isObjectType(data)) {
|
|
748
841
|
const dataKeys = Object.keys(data);
|
|
749
|
-
|
|
750
|
-
|
|
842
|
+
const primitives = validatePrimitives(data, dataPath, dataRoot, dataKeys);
|
|
843
|
+
return validateChildren(data, dataPath, dataRoot, dataKeys) && primitives;
|
|
751
844
|
}
|
|
752
845
|
return true;
|
|
753
846
|
};
|
package/src/query.js
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
|
|
13
13
|
import { JarenValidator } from './index.js';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Project a diagnostic string from whatever the validator threw, without
|
|
17
|
+
* reading `.message` off a raw value or coercing it.
|
|
18
|
+
* @param {unknown} e
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function failureText(e) {
|
|
22
|
+
if (e instanceof Error && typeof e.message === 'string')
|
|
23
|
+
return e.message;
|
|
24
|
+
return typeof e === 'string' ? e : 'schema compilation failed';
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
/**
|
|
16
28
|
* Create a `compileTypeTest` hook for `compileJsonQuery` (see
|
|
17
29
|
* `@jarenjs/json/query`), backed by a `JarenValidator`.
|
|
@@ -44,8 +56,22 @@ export function createTypeTestCompiler(validator = undefined) {
|
|
|
44
56
|
const instance = validator == null
|
|
45
57
|
? new JarenValidator()
|
|
46
58
|
: (typeof validator === 'function' ? validator() : validator);
|
|
47
|
-
return function compileTypeTest(schemaJson) {
|
|
48
|
-
|
|
59
|
+
return function compileTypeTest(schemaJson, docPath) {
|
|
60
|
+
let validate;
|
|
61
|
+
try {
|
|
62
|
+
validate = instance.compile(schemaJson);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// The engine's JQ0009 names the operator that owns the schema; this
|
|
66
|
+
// names the schema literal itself, which is what distinguishes one
|
|
67
|
+
// failing schema from the others in the same query document.
|
|
68
|
+
const where = typeof docPath === 'string' && docPath !== '' ? docPath : '';
|
|
69
|
+
throw new Error(
|
|
70
|
+
where === ''
|
|
71
|
+
? failureText(err)
|
|
72
|
+
: `schema literal at '${where}': ${failureText(err)}`,
|
|
73
|
+
{ cause: err });
|
|
74
|
+
}
|
|
49
75
|
// The default validator options are boolean mode (skipErrors on,
|
|
50
76
|
// collectErrors off): the compiled function IS the predicate. An
|
|
51
77
|
// error-collecting instance returns { valid, errors } objects
|
package/src/schema.js
CHANGED
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
getStringType,
|
|
6
6
|
isObjectClass,
|
|
7
7
|
} from '@jarenjs/core';
|
|
8
|
+
import {
|
|
9
|
+
NUMERIC_CONSTRAINTS, STRING_CONSTRAINTS,
|
|
10
|
+
ARRAY_CONSTRAINTS, OBJECT_CONSTRAINTS,
|
|
11
|
+
} from '@jarenjs/core/schema';
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
14
|
getUniqueArray,
|
|
@@ -15,20 +19,23 @@ import {
|
|
|
15
19
|
} from '@jarenjs/core/number';
|
|
16
20
|
|
|
17
21
|
import {
|
|
18
|
-
falseThat,
|
|
19
22
|
trueThat,
|
|
20
23
|
addFunctionToArray,
|
|
21
24
|
} from '@jarenjs/core/function';
|
|
22
25
|
|
|
23
26
|
import {
|
|
27
|
+
combineIndependent,
|
|
24
28
|
createIsSchemaTypeHandler,
|
|
29
|
+
hasSchemaRef,
|
|
30
|
+
hasSchemaRecursiveRef,
|
|
31
|
+
hasSchemaDynamicRef,
|
|
25
32
|
} from './tools.js';
|
|
26
33
|
|
|
27
34
|
import {
|
|
28
35
|
getStringLength,
|
|
29
|
-
getSegmenter,
|
|
30
36
|
} from '@jarenjs/core/string';
|
|
31
37
|
|
|
38
|
+
import { compileErrorMessageSpec } from './messages.js';
|
|
32
39
|
import { compileFormatBasic } from './format.js';
|
|
33
40
|
import { compileEnumBasic } from './enum.js';
|
|
34
41
|
import { compileNumberBasic } from './number.js';
|
|
@@ -41,9 +48,8 @@ import { compileCombineSchema } from './combine.js';
|
|
|
41
48
|
import { compileConditionSchema } from './condition.js';
|
|
42
49
|
import { compileDataSchema } from './data.js';
|
|
43
50
|
import { compileQuerySchema } from './query-keyword.js';
|
|
44
|
-
import { compileDollarDataSchema
|
|
51
|
+
import { compileDollarDataSchema } from './dollar-data.js';
|
|
45
52
|
import { wrapUnevaluated } from './unevaluated.js';
|
|
46
|
-
import { hasSchemaRef, hasSchemaRecursiveRef, hasSchemaDynamicRef } from './tools.js';
|
|
47
53
|
import { createJsonPointer } from './traverse.js';
|
|
48
54
|
|
|
49
55
|
/**
|
|
@@ -168,7 +174,7 @@ function compileDynamicAnchorRef(schemaObj, jsonSchema) {
|
|
|
168
174
|
let targetObj;
|
|
169
175
|
try {
|
|
170
176
|
targetObj = root.resolveObject(resolvedRef, baseUri, { $ref: resolvedRef });
|
|
171
|
-
} catch (
|
|
177
|
+
} catch (_e) {
|
|
172
178
|
return addError(data, dataPath);
|
|
173
179
|
}
|
|
174
180
|
if (targetObj) {
|
|
@@ -414,6 +420,20 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
414
420
|
throw new Error('JSON Schema MUST be a boolean or Object Type');
|
|
415
421
|
|
|
416
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
|
+
|
|
417
437
|
if (keys.length === 0)
|
|
418
438
|
return trueThat;
|
|
419
439
|
|
|
@@ -423,7 +443,9 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
423
443
|
// We need to check the actual behavior based on schema context.
|
|
424
444
|
// If schema has ONLY $ref (and meta keywords), use the ref-only path.
|
|
425
445
|
// If schema has $ref with validation siblings, process them together (2019-09+ only).
|
|
426
|
-
|
|
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;
|
|
427
449
|
// When compiling the sibling keywords of a $ref schema, the unevaluated*
|
|
428
450
|
// wrapper is applied by ValidationObject.compileValidator around the
|
|
429
451
|
// combined (ref + siblings) validator instead of here, so that the
|
|
@@ -431,13 +453,16 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
431
453
|
let refWithSiblings = false;
|
|
432
454
|
if (hasSchemaRef(jsonSchema) && !hasSchemaRecursiveRef(jsonSchema)) {
|
|
433
455
|
// Check if there are any validation-related sibling keywords
|
|
434
|
-
// In draft 2019-09+, if there are validation siblings, we process
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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',
|
|
438
463
|
'dependentRequired', 'properties', 'patternProperties', 'additionalProperties', 'items',
|
|
439
464
|
'prefixItems', 'additionalItems', 'contains', 'allOf', 'anyOf', 'oneOf', 'not', 'if',
|
|
440
|
-
'then', 'else', 'propertyNames', '
|
|
465
|
+
'then', 'else', 'propertyNames', 'contentEncoding', 'contentMediaType',
|
|
441
466
|
'unevaluatedProperties', 'unevaluatedItems', '$query'];
|
|
442
467
|
const hasValidationSiblings = keys.some(k => validationKeywords.includes(k));
|
|
443
468
|
// In draft 7 and earlier, $ref always overrides siblings regardless
|
|
@@ -449,10 +474,6 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
449
474
|
refWithSiblings = true;
|
|
450
475
|
}
|
|
451
476
|
|
|
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
477
|
// When the metaschema's $vocabulary omits the validation vocabulary,
|
|
457
478
|
// keywords like type/enum/minimum/minLength assert nothing.
|
|
458
479
|
const vocabValidation = schemaObj.options.vocabValidation !== false;
|
|
@@ -461,7 +482,7 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
461
482
|
// These inline the validation to reduce function call overhead
|
|
462
483
|
|
|
463
484
|
// Fast path: type-only schema (most common case: {"type": "string"})
|
|
464
|
-
if (vocabValidation &&
|
|
485
|
+
if (vocabValidation && keyCount === 1 && jsonSchema.type !== undefined) {
|
|
465
486
|
const type = jsonSchema.type;
|
|
466
487
|
// Only handle single type strings here (not arrays of types)
|
|
467
488
|
if (typeof type === 'string') {
|
|
@@ -502,29 +523,45 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
502
523
|
}
|
|
503
524
|
|
|
504
525
|
// Fast path: required-only schema (common case: {"required": ["foo", "bar"]})
|
|
505
|
-
if (vocabValidation &&
|
|
526
|
+
if (vocabValidation && keyCount === 1 && jsonSchema.required !== undefined) {
|
|
506
527
|
const required = jsonSchema.required;
|
|
507
|
-
|
|
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')) {
|
|
508
532
|
const addError = schemaObj.createErrorHandler(required, ['required']);
|
|
509
533
|
const rlen = required.length;
|
|
510
534
|
|
|
511
|
-
|
|
512
|
-
|
|
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) {
|
|
513
551
|
if (typeof data !== 'object' || data === null || Array.isArray(data)) return true;
|
|
514
|
-
|
|
552
|
+
let valid = true;
|
|
515
553
|
for (let i = 0; i < rlen; i++) {
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
}
|
|
554
|
+
if (!Object.hasOwn(data, required[i]))
|
|
555
|
+
valid = addError(required[i], data, dataPath) && valid;
|
|
519
556
|
}
|
|
520
|
-
return
|
|
557
|
+
return valid;
|
|
521
558
|
};
|
|
522
559
|
}
|
|
523
560
|
}
|
|
524
561
|
|
|
525
562
|
// Fast path: minLength-only schema (common case: {"minLength": 2})
|
|
526
563
|
// This avoids the overhead of compileStringBasic for simple cases
|
|
527
|
-
if (vocabValidation &&
|
|
564
|
+
if (vocabValidation && keyCount === 1 && jsonSchema.minLength !== undefined) {
|
|
528
565
|
const min = jsonSchema.minLength;
|
|
529
566
|
if (typeof min === 'number' && min > 0 && Number.isFinite(min)) {
|
|
530
567
|
const addError = schemaObj.createErrorHandler(min, 'minLength');
|
|
@@ -548,7 +585,7 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
548
585
|
}
|
|
549
586
|
|
|
550
587
|
// Fast path: maxLength-only schema (common case: {"maxLength": 10})
|
|
551
|
-
if (vocabValidation &&
|
|
588
|
+
if (vocabValidation && keyCount === 1 && jsonSchema.maxLength !== undefined) {
|
|
552
589
|
const max = jsonSchema.maxLength;
|
|
553
590
|
if (typeof max === 'number' && max >= 0 && Number.isFinite(max)) {
|
|
554
591
|
const addError = schemaObj.createErrorHandler(max, 'maxLength');
|
|
@@ -613,6 +650,14 @@ export function compileSchemaObject(schemaObj, jsonSchema) {
|
|
|
613
650
|
if (validators.length === 1)
|
|
614
651
|
return finalize(validators[0]);
|
|
615
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
|
+
|
|
616
661
|
if (validators.length === 2) {
|
|
617
662
|
const first = validators[0];
|
|
618
663
|
const second = validators[1];
|
package/src/string.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
isStringType,
|
|
5
|
-
getStringType,
|
|
6
5
|
} from '@jarenjs/core';
|
|
7
6
|
|
|
8
7
|
import {
|
|
@@ -12,7 +11,6 @@ import {
|
|
|
12
11
|
import {
|
|
13
12
|
createRegExp,
|
|
14
13
|
getStringLength,
|
|
15
|
-
getSegmenter,
|
|
16
14
|
} from '@jarenjs/core/string';
|
|
17
15
|
|
|
18
16
|
import {
|
|
@@ -70,11 +68,25 @@ function compileStringIntern(schemaObj, jsonSchema) {
|
|
|
70
68
|
const isMatch = pattern || trueThat;
|
|
71
69
|
const useGrapheme = schemaObj.options.useGrapheme;
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
if (schemaObj.options.skipErrors) {
|
|
72
|
+
return function validateStringIntern(data, dataPath) {
|
|
73
|
+
const len = getStringLength(data, useGrapheme);
|
|
74
|
+
return isMinLength(len, dataPath)
|
|
75
|
+
&& isMaxLength(len, dataPath)
|
|
76
|
+
&& isMatch(data, dataPath);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Length and pattern are independent assertions over the same string:
|
|
81
|
+
// `len` is computed before any of them and `isMatch` reads the raw data,
|
|
82
|
+
// so a failed `minLength` says nothing about whether `pattern` holds.
|
|
83
|
+
// Short-circuiting them is a boolean-mode optimization; when errors are
|
|
84
|
+
// recorded it would hide half the reasons the value is wrong.
|
|
85
|
+
return function validateStringInternAll(data, dataPath) {
|
|
74
86
|
const len = getStringLength(data, useGrapheme);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
let valid = isMinLength(len, dataPath);
|
|
88
|
+
valid = isMaxLength(len, dataPath) && valid;
|
|
89
|
+
return isMatch(data, dataPath) && valid;
|
|
78
90
|
};
|
|
79
91
|
}
|
|
80
92
|
|