@jarenjs/validate 0.9.2 → 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.
- 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/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
isBoolOrObjectClass,
|
|
23
23
|
hasSchemaRef,
|
|
24
|
+
hasUnevaluatedPropertiesCoverage,
|
|
25
|
+
hasUnevaluatedItemsCoverage,
|
|
24
26
|
EvalLog,
|
|
25
27
|
} from './tools.js';
|
|
26
28
|
import { wrapUnevaluated } from './unevaluated.js';
|
|
@@ -32,6 +34,20 @@ export {
|
|
|
32
34
|
registerFormatCompilers
|
|
33
35
|
} from './format.js';
|
|
34
36
|
|
|
37
|
+
import {
|
|
38
|
+
convertInternalErrors,
|
|
39
|
+
} from './messages.js';
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
ValidationError,
|
|
43
|
+
messagesEn,
|
|
44
|
+
compileMessageTemplate,
|
|
45
|
+
compileMessageCatalog,
|
|
46
|
+
renderErrorMessage,
|
|
47
|
+
localizeErrors,
|
|
48
|
+
} from './messages.js';
|
|
49
|
+
|
|
50
|
+
|
|
35
51
|
export { TraverseOptions };
|
|
36
52
|
|
|
37
53
|
/**
|
|
@@ -206,43 +222,6 @@ class InternalValidationError {
|
|
|
206
222
|
}
|
|
207
223
|
}
|
|
208
224
|
|
|
209
|
-
/**
|
|
210
|
-
* JSON Schema Validation Error
|
|
211
|
-
* Represents a validation error according to the JSON Schema specification.
|
|
212
|
-
* @see https://json-schema.org/draft/2020-12/json-schema-core.html#output
|
|
213
|
-
*/
|
|
214
|
-
export class ValidationError {
|
|
215
|
-
/**
|
|
216
|
-
* @param {object} options - Error options
|
|
217
|
-
* @param {string} options.keyword - The keyword that failed validation
|
|
218
|
-
* @param {string} options.instancePath - JSON Pointer to the data location
|
|
219
|
-
* @param {string} options.schemaPath - JSON Pointer to the schema location
|
|
220
|
-
* @param {object} options.params - Keyword-specific parameters
|
|
221
|
-
* @param {string} [options.message] - Human-readable error message
|
|
222
|
-
*/
|
|
223
|
-
constructor(options) {
|
|
224
|
-
this.keyword = options.keyword;
|
|
225
|
-
this.instancePath = options.instancePath || '';
|
|
226
|
-
this.schemaPath = options.schemaPath || '';
|
|
227
|
-
this.params = options.params || {};
|
|
228
|
-
this.message = options.message || '';
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Convert error to a plain object
|
|
233
|
-
* @returns {object} Plain object representation
|
|
234
|
-
*/
|
|
235
|
-
toJSON() {
|
|
236
|
-
return {
|
|
237
|
-
keyword: this.keyword,
|
|
238
|
-
instancePath: this.instancePath,
|
|
239
|
-
schemaPath: this.schemaPath,
|
|
240
|
-
params: this.params,
|
|
241
|
-
message: this.message,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
225
|
/**
|
|
247
226
|
* ValidationOptions configures the behavior of the validation process.
|
|
248
227
|
* @class
|
|
@@ -257,6 +236,8 @@ export class ValidationOptions {
|
|
|
257
236
|
* @param {number} [draftVersion=7] - The JSON Schema draft version (6, 7, 2019, or 2020)
|
|
258
237
|
* @param {boolean} [vocabValidation=true] - Whether the validation vocabulary is enabled (false when the schema's metaschema omits it via $vocabulary)
|
|
259
238
|
* @param {boolean|null} [formatAssertion=null] - Whether format asserts (null = auto: asserts below draft 2020-12, annotation-only from 2020-12 on)
|
|
239
|
+
* @param {boolean} [messages=true] - Whether collected errors carry rendered message text; false skips rendering (message: '', params/msgid still set)
|
|
240
|
+
* @param {'error'|'ignore'} [unknownFormats='ignore'] - What to do when an ASSERTING `format` names something no compiler is registered for: 'ignore' (the default, and what the specification requires) accepts it as an annotation; 'error' throws at COMPILE time. Never affects instance validation, and never applies where format is annotation-only anyway.
|
|
260
241
|
*/
|
|
261
242
|
constructor(
|
|
262
243
|
skipErrors = true,
|
|
@@ -265,7 +246,9 @@ export class ValidationOptions {
|
|
|
265
246
|
contentValidation = null,
|
|
266
247
|
draftVersion = 7,
|
|
267
248
|
vocabValidation = true,
|
|
268
|
-
formatAssertion = null
|
|
249
|
+
formatAssertion = null,
|
|
250
|
+
messages = true,
|
|
251
|
+
unknownFormats = 'ignore'
|
|
269
252
|
) {
|
|
270
253
|
/** @type {boolean} Whether to stop at first error or continue */
|
|
271
254
|
this.skipErrors = skipErrors;
|
|
@@ -281,6 +264,10 @@ export class ValidationOptions {
|
|
|
281
264
|
this.vocabValidation = vocabValidation;
|
|
282
265
|
/** @type {boolean|null} Whether the format keyword asserts (null = auto by draft) */
|
|
283
266
|
this.formatAssertion = formatAssertion;
|
|
267
|
+
/** @type {boolean} Whether collected errors carry rendered message text */
|
|
268
|
+
this.messages = messages;
|
|
269
|
+
/** @type {'error'|'ignore'} What an asserting `format` with no registered compiler does */
|
|
270
|
+
this.unknownFormats = unknownFormats;
|
|
284
271
|
}
|
|
285
272
|
}
|
|
286
273
|
|
|
@@ -343,6 +330,8 @@ export class ValidationRoot {
|
|
|
343
330
|
#evalLog = new EvalLog();
|
|
344
331
|
/** @type {object|null} The JarenValidator instance this compilation belongs to, or null when constructed standalone */
|
|
345
332
|
#owner = null;
|
|
333
|
+
/** @type {Map<string, object>|null} Compiled 'errorMessage' specs by schema path; null when the schema set has none */
|
|
334
|
+
#errorMessages = null;
|
|
346
335
|
|
|
347
336
|
/** Keywords whose value is a map of arbitrary names to schemas; those
|
|
348
337
|
* names must not be mistaken for keywords (e.g. a metaschema declaring
|
|
@@ -352,6 +341,27 @@ export class ValidationRoot {
|
|
|
352
341
|
'$defs', 'definitions',
|
|
353
342
|
]);
|
|
354
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Whether an unevaluatedProperties/unevaluatedItems occurrence can force
|
|
346
|
+
* runtime annotation tracking. Two shapes never can (in skipErrors mode):
|
|
347
|
+
* the literal `true` form asserts nothing and only produces annotations,
|
|
348
|
+
* which matter only when a checking occurrence elsewhere consumes them;
|
|
349
|
+
* and a check whose sibling keywords already evaluate every property/item
|
|
350
|
+
* (see hasUnevaluatedPropertiesCoverage/hasUnevaluatedItemsCoverage) is
|
|
351
|
+
* unreachable, because reaching it means those siblings passed. When no
|
|
352
|
+
* occurrence forces tracking, the evaluation log has no consumers and
|
|
353
|
+
* annotation logging is skipped entirely.
|
|
354
|
+
* @param {object} node - The schema object holding the keyword
|
|
355
|
+
* @param {string} key - 'unevaluatedProperties' or 'unevaluatedItems'
|
|
356
|
+
* @returns {boolean} True when this occurrence requires annotation tracking
|
|
357
|
+
*/
|
|
358
|
+
static #unevaluatedForcesTracking(node, key) {
|
|
359
|
+
if (node[key] === true) return false;
|
|
360
|
+
return key === 'unevaluatedProperties'
|
|
361
|
+
? !hasUnevaluatedPropertiesCoverage(node)
|
|
362
|
+
: !hasUnevaluatedItemsCoverage(node);
|
|
363
|
+
}
|
|
364
|
+
|
|
355
365
|
/**
|
|
356
366
|
* Recursively scans a schema (sub)tree for keys that require special
|
|
357
367
|
* runtime support: '$data' references and 'unevaluatedProperties'/
|
|
@@ -390,7 +400,16 @@ export class ValidationRoot {
|
|
|
390
400
|
flags.dollarData = true;
|
|
391
401
|
continue;
|
|
392
402
|
}
|
|
393
|
-
else if (key === '
|
|
403
|
+
else if (key === 'errorMessage') {
|
|
404
|
+
// 'errorMessage' is report-time metadata; its value is a message
|
|
405
|
+
// spec whose map form may spell keys like '$query' that must not
|
|
406
|
+
// register as keywords of this compilation.
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
else if (key === 'unevaluatedProperties' || key === 'unevaluatedItems') {
|
|
410
|
+
if (!flags.canElide || ValidationRoot.#unevaluatedForcesTracking(node, key))
|
|
411
|
+
flags.unevaluated = true;
|
|
412
|
+
}
|
|
394
413
|
if (ValidationRoot.#SCAN_MAP_KEYWORDS.has(key)) {
|
|
395
414
|
// The value is a name->schema map: its keys are names, its values schemas.
|
|
396
415
|
ValidationRoot.#scanSchemaFeatures(node[key], seen, flags, false);
|
|
@@ -429,7 +448,10 @@ export class ValidationRoot {
|
|
|
429
448
|
// Detect $data references and unevaluated* keywords once, so fast paths
|
|
430
449
|
// can skip path building / annotation logging when nothing consumes them.
|
|
431
450
|
// Must run before validators are compiled below.
|
|
432
|
-
|
|
451
|
+
// Elision of unreachable unevaluated* checks relies on validators
|
|
452
|
+
// short-circuiting at the first failure, so it only holds in
|
|
453
|
+
// skipErrors mode (see #unevaluatedForcesTracking).
|
|
454
|
+
const flags = { dollarData: false, unevaluated: false, canElide: opts.skipErrors === true };
|
|
433
455
|
const seen = new Set();
|
|
434
456
|
for (const value of schemas.values()) {
|
|
435
457
|
ValidationRoot.#scanSchemaFeatures(value, seen, flags);
|
|
@@ -455,9 +477,6 @@ export class ValidationRoot {
|
|
|
455
477
|
.filter(anchor => anchor.schema !== rootSchema);
|
|
456
478
|
}
|
|
457
479
|
|
|
458
|
-
/** @returns {string} The root schema origin/URI */
|
|
459
|
-
get rootOrigin() { return this.#rootOrigin; }
|
|
460
|
-
|
|
461
480
|
/** @returns {TraverseOptions} Schema traversal options */
|
|
462
481
|
get traverse() { return this.#traverse; }
|
|
463
482
|
|
|
@@ -470,9 +489,6 @@ export class ValidationRoot {
|
|
|
470
489
|
/** @returns {Array} Array of validation errors */
|
|
471
490
|
get errors() { return this.#errors; }
|
|
472
491
|
|
|
473
|
-
/** @returns {ValidationObject} The root schema's ValidationObject */
|
|
474
|
-
get firstSchema() { return this.#firstSchema; }
|
|
475
|
-
|
|
476
492
|
/** @returns {boolean} Whether any schema in this compilation contains a $data reference */
|
|
477
493
|
get usesDollarData() { return this.#usesDollarData; }
|
|
478
494
|
|
|
@@ -485,6 +501,21 @@ export class ValidationRoot {
|
|
|
485
501
|
/** @returns {object|null} The owning JarenValidator instance, or null when constructed standalone */
|
|
486
502
|
get owner() { return this.#owner; }
|
|
487
503
|
|
|
504
|
+
/** @returns {Map<string, object>|null} Compiled 'errorMessage' specs by schema path, or null when the schema set has none */
|
|
505
|
+
get errorMessages() { return this.#errorMessages; }
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Register a compiled 'errorMessage' spec for a schema location.
|
|
509
|
+
* Called at schema compile time (see compileSchemaObject); the registry
|
|
510
|
+
* is only consulted at report time, over the already-failed set.
|
|
511
|
+
* @param {string} path - The schema path (ValidationObject.path)
|
|
512
|
+
* @param {object} spec - The compiled spec (see messages.js compileErrorMessageSpec)
|
|
513
|
+
*/
|
|
514
|
+
registerErrorMessage(path, spec) {
|
|
515
|
+
if (this.#errorMessages === null) this.#errorMessages = new Map();
|
|
516
|
+
this.#errorMessages.set(path, spec);
|
|
517
|
+
}
|
|
518
|
+
|
|
488
519
|
/**
|
|
489
520
|
* Creates a new ValidationObject for the given path and schema.
|
|
490
521
|
* @param {string} path - The URI path for this schema object
|
|
@@ -510,24 +541,6 @@ export class ValidationRoot {
|
|
|
510
541
|
return null;
|
|
511
542
|
}
|
|
512
543
|
|
|
513
|
-
/**
|
|
514
|
-
* Gets the raw schema object for a given reference without compiling it.
|
|
515
|
-
* Used to check schema properties (like $recursiveAnchor) at compile time.
|
|
516
|
-
* @param {string} ref - The reference URI to resolve
|
|
517
|
-
* @param {string} path - The current path (for error messages)
|
|
518
|
-
* @param {object} schema - The schema containing the $ref
|
|
519
|
-
* @returns {{id: string, schema: object}|null} The resolved schema info or null
|
|
520
|
-
*/
|
|
521
|
-
getRawSchema(ref, path, schema) {
|
|
522
|
-
try {
|
|
523
|
-
const schemas = this.#schemas;
|
|
524
|
-
const traverse = this.#traverse;
|
|
525
|
-
return resolveRefSchemaDeep(schemas, path, schema, traverse);
|
|
526
|
-
} catch (e) {
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
544
|
/**
|
|
532
545
|
* Gets the raw schema object by its URI/ID directly from the schemas map.
|
|
533
546
|
* This performs a direct lookup without following references.
|
|
@@ -574,6 +587,30 @@ export class ValidationRoot {
|
|
|
574
587
|
return false;
|
|
575
588
|
}
|
|
576
589
|
|
|
590
|
+
/**
|
|
591
|
+
* A checkpoint in the collected-error list.
|
|
592
|
+
*
|
|
593
|
+
* A SPECULATIVE applicator - an `anyOf` branch, an `if` condition, the
|
|
594
|
+
* subschema of a `not`, a `contains` candidate - runs a validator whose
|
|
595
|
+
* failure may be entirely expected. Those failures still call `addError`,
|
|
596
|
+
* so without a checkpoint they leak into the caller's issue list and blame
|
|
597
|
+
* a document for not matching a branch it was never required to match.
|
|
598
|
+
* Marking before the probe and rolling back after is the same discipline
|
|
599
|
+
* `EvalLog` already uses for annotations.
|
|
600
|
+
* @returns {number} The mark to pass to {@link rollbackErrors}
|
|
601
|
+
*/
|
|
602
|
+
errorMark() {
|
|
603
|
+
return this.#errors.length;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Discard every error collected since `mark`.
|
|
608
|
+
* @param {number} mark - A value from {@link errorMark}
|
|
609
|
+
*/
|
|
610
|
+
rollbackErrors(mark) {
|
|
611
|
+
if (this.#errors.length > mark) this.#errors.length = mark;
|
|
612
|
+
}
|
|
613
|
+
|
|
577
614
|
/**
|
|
578
615
|
* Validates data against the root schema.
|
|
579
616
|
* @param {unknown} data - The data to validate
|
|
@@ -624,6 +661,36 @@ export class ValidationRoot {
|
|
|
624
661
|
return rootValidator(data, '', data);
|
|
625
662
|
}
|
|
626
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Returns the fastest repeated-validation entry point for this root.
|
|
666
|
+
* Error collection and root-level dynamic anchors need the per-call
|
|
667
|
+
* bookkeeping of validate(); without them the compiled root validator
|
|
668
|
+
* only needs the annotation log cleared (when tracking is on) and can
|
|
669
|
+
* otherwise be invoked directly. Dynamic anchors pushed during
|
|
670
|
+
* validation are balanced by try/finally, so the anchor map needs no
|
|
671
|
+
* per-call clearing here.
|
|
672
|
+
* @returns {(data: unknown) => boolean} The validation entry point
|
|
673
|
+
*/
|
|
674
|
+
createValidateFn() {
|
|
675
|
+
if (!this.#options.skipErrors || this.#options.collectErrors
|
|
676
|
+
|| this.#rootAnchorName !== null
|
|
677
|
+
|| this.#rootDynamicAnchors.length !== 0) {
|
|
678
|
+
return (data) => this.validate(data);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const rootValidator = this.#rootValidator;
|
|
682
|
+
if (this.#usesUnevaluated) {
|
|
683
|
+
const evalLog = this.#evalLog;
|
|
684
|
+
return function validateRootTracked(data) {
|
|
685
|
+
evalLog.reset();
|
|
686
|
+
return rootValidator(data, '', data);
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
return function validateRoot(data) {
|
|
690
|
+
return rootValidator(data, '', data);
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
627
694
|
/**
|
|
628
695
|
* Get the stored validator for a dynamic anchor.
|
|
629
696
|
* Used by $dynamicRef for runtime resolution.
|
|
@@ -729,7 +796,13 @@ export class ValidationObject {
|
|
|
729
796
|
|
|
730
797
|
// In draft 2019-09+, $ref can have sibling keywords that are applied together.
|
|
731
798
|
// In draft 7 and earlier, $ref overrides siblings.
|
|
732
|
-
|
|
799
|
+
//
|
|
800
|
+
// The draft that decides this is the one declared by the schema RESOURCE
|
|
801
|
+
// holding the `$ref`, not the one the root document happens to use. A
|
|
802
|
+
// 2020-12 resource embedded in a draft-07 document has to assert its
|
|
803
|
+
// siblings, and a draft-07 resource inside a 2020-12 document must not —
|
|
804
|
+
// reading the root's draft got both backwards.
|
|
805
|
+
const draftVersion = self.declaredDraft ?? root.options.draftVersion ?? 7;
|
|
733
806
|
|
|
734
807
|
// Base URI for resolving $ref:
|
|
735
808
|
// - Draft 7 and earlier: $ref replaces the schema entirely, so a sibling
|
|
@@ -795,7 +868,8 @@ export class ValidationObject {
|
|
|
795
868
|
root.pushDynamicAnchorValidator(anchorName, refValidator);
|
|
796
869
|
}
|
|
797
870
|
try {
|
|
798
|
-
return refValidator
|
|
871
|
+
return combineRefAndSiblings(refValidator, siblingValidator, self.options.skipErrors,
|
|
872
|
+
data, dataPath, dataRoot);
|
|
799
873
|
} finally {
|
|
800
874
|
// Pop in reverse order
|
|
801
875
|
if (hasRecAnchor || dynAnchorName) {
|
|
@@ -839,7 +913,8 @@ export class ValidationObject {
|
|
|
839
913
|
// unevaluated* keywords must see annotations produced by the $ref
|
|
840
914
|
// target, so the wrapper goes around the combined validator.
|
|
841
915
|
return wrapUnevaluated(self, schema, function validateRefWithSiblings(data, dataPath, dataRoot) {
|
|
842
|
-
return refValidator
|
|
916
|
+
return combineRefAndSiblings(refValidator, siblingValidator, self.options.skipErrors,
|
|
917
|
+
data, dataPath, dataRoot);
|
|
843
918
|
});
|
|
844
919
|
}
|
|
845
920
|
|
|
@@ -898,7 +973,8 @@ export class ValidationObject {
|
|
|
898
973
|
try {
|
|
899
974
|
// If there are sibling validators (draft 2019-09+), combine them with the ref validator
|
|
900
975
|
if (boundSiblingValidator) {
|
|
901
|
-
return refValidator
|
|
976
|
+
return combineRefAndSiblings(refValidator, boundSiblingValidator,
|
|
977
|
+
self.options.skipErrors, data, dataPath, dataRoot);
|
|
902
978
|
}
|
|
903
979
|
return refValidator(data, dataPath, dataRoot);
|
|
904
980
|
} finally {
|
|
@@ -908,7 +984,8 @@ export class ValidationObject {
|
|
|
908
984
|
|
|
909
985
|
// If there are sibling validators (draft 2019-09+), combine them with the ref validator
|
|
910
986
|
if (boundSiblingValidator) {
|
|
911
|
-
return refValidator
|
|
987
|
+
return combineRefAndSiblings(refValidator, boundSiblingValidator,
|
|
988
|
+
self.options.skipErrors, data, dataPath, dataRoot);
|
|
912
989
|
}
|
|
913
990
|
|
|
914
991
|
// Cache the validator directly (skipping this resolver) only when
|
|
@@ -937,8 +1014,6 @@ export class ValidationObject {
|
|
|
937
1014
|
#schema = null;
|
|
938
1015
|
/** @type {function|null} The compiled validator function */
|
|
939
1016
|
#validator = null;
|
|
940
|
-
/** @type {string} The base URI passed during construction */
|
|
941
|
-
#baseUri = null;
|
|
942
1017
|
/** @type {string} The effective base URI for child $ref resolution */
|
|
943
1018
|
#effectiveBaseUri = null;
|
|
944
1019
|
/** @type {number|null} Draft version declared by this schema's document ($schema), inherited by subschemas; null when never declared */
|
|
@@ -977,7 +1052,6 @@ export class ValidationObject {
|
|
|
977
1052
|
// No $id - inherit parent's base
|
|
978
1053
|
this.#effectiveBaseUri = baseUri;
|
|
979
1054
|
}
|
|
980
|
-
this.#baseUri = baseUri;
|
|
981
1055
|
|
|
982
1056
|
this.#validator = ValidationObject.compileValidator(this, path, schema, baseUri);
|
|
983
1057
|
}
|
|
@@ -992,11 +1066,6 @@ export class ValidationObject {
|
|
|
992
1066
|
return this.#effectiveBaseUri;
|
|
993
1067
|
}
|
|
994
1068
|
|
|
995
|
-
/** @returns {Array} The current validation errors from the root */
|
|
996
|
-
get errors() {
|
|
997
|
-
return this.#root.errors;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1069
|
/** @returns {function} The compiled validator function */
|
|
1001
1070
|
get validate() {
|
|
1002
1071
|
return this.#validator;
|
|
@@ -1040,13 +1109,13 @@ export class ValidationObject {
|
|
|
1040
1109
|
// Just return false immediately to avoid the overhead of error creation
|
|
1041
1110
|
if (self.#root.options.skipErrors) {
|
|
1042
1111
|
if (!Array.isArray(key)) {
|
|
1043
|
-
return function addNormalErrorFast(
|
|
1112
|
+
return function addNormalErrorFast(_data, ..._meta) {
|
|
1044
1113
|
// Just return false without creating error object
|
|
1045
1114
|
return false;
|
|
1046
1115
|
};
|
|
1047
1116
|
}
|
|
1048
1117
|
else {
|
|
1049
|
-
return function addKeyedErrorFast(
|
|
1118
|
+
return function addKeyedErrorFast(_dataKey, _data, ..._meta) {
|
|
1050
1119
|
// Just return false without creating error object
|
|
1051
1120
|
return false;
|
|
1052
1121
|
};
|
|
@@ -1118,6 +1187,26 @@ export class ValidationObject {
|
|
|
1118
1187
|
}
|
|
1119
1188
|
}
|
|
1120
1189
|
|
|
1190
|
+
/**
|
|
1191
|
+
* Run a `$ref` and its sibling keywords, which are INDEPENDENT of each other:
|
|
1192
|
+
* a document can fail the referenced schema and its siblings for unrelated
|
|
1193
|
+
* reasons, and reporting only the first is the same short-circuit that used to
|
|
1194
|
+
* hide half of every issue list. Boolean mode keeps the early exit.
|
|
1195
|
+
* @param {Function} refValidator
|
|
1196
|
+
* @param {Function} siblingValidator
|
|
1197
|
+
* @param {boolean} stopAtFirst
|
|
1198
|
+
* @param {any} data
|
|
1199
|
+
* @param {string} dataPath
|
|
1200
|
+
* @param {any} dataRoot
|
|
1201
|
+
* @returns {boolean}
|
|
1202
|
+
*/
|
|
1203
|
+
function combineRefAndSiblings(refValidator, siblingValidator, stopAtFirst, data, dataPath, dataRoot) {
|
|
1204
|
+
if (stopAtFirst)
|
|
1205
|
+
return refValidator(data, dataPath, dataRoot) && siblingValidator(data, dataPath, dataRoot);
|
|
1206
|
+
const target = refValidator(data, dataPath, dataRoot);
|
|
1207
|
+
return siblingValidator(data, dataPath, dataRoot) && target;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1121
1210
|
/**
|
|
1122
1211
|
* ValidatorOptions configures the JarenValidator instance.
|
|
1123
1212
|
* Can be created with positional arguments or an options object.
|
|
@@ -1156,17 +1245,22 @@ export class ValidatorOptions {
|
|
|
1156
1245
|
/** @type {object[]} Initial schemas to register */
|
|
1157
1246
|
this.schemas = opts.schemas || [];
|
|
1158
1247
|
// If collectErrors is passed directly, create ValidationOptions with it
|
|
1159
|
-
if (opts.collectErrors != null || opts.skipErrors != null || opts.useGrapheme != null || opts.contentValidation != null || opts.draftVersion != null || opts.formatAssertion != null) {
|
|
1248
|
+
if (opts.collectErrors != null || opts.skipErrors != null || opts.useGrapheme != null || opts.contentValidation != null || opts.draftVersion != null || opts.formatAssertion != null || opts.messages != null || opts.unknownFormats != null) {
|
|
1160
1249
|
const collectErrors = opts.collectErrors ?? false;
|
|
1161
1250
|
this.validation = new ValidationOptions(
|
|
1162
1251
|
// collecting errors implies actually recording them
|
|
1163
1252
|
opts.skipErrors ?? !collectErrors,
|
|
1164
1253
|
opts.useGrapheme ?? true,
|
|
1165
1254
|
collectErrors,
|
|
1166
|
-
|
|
1255
|
+
// null, not false: an unset option must stay unset so `compile`
|
|
1256
|
+
// can apply the per-draft default. Coercing it here would make
|
|
1257
|
+
// any options object silently disable content assertion.
|
|
1258
|
+
opts.contentValidation ?? null,
|
|
1167
1259
|
opts.draftVersion ?? 7,
|
|
1168
1260
|
true,
|
|
1169
|
-
opts.formatAssertion ?? null
|
|
1261
|
+
opts.formatAssertion ?? null,
|
|
1262
|
+
opts.messages ?? true,
|
|
1263
|
+
opts.unknownFormats ?? 'ignore'
|
|
1170
1264
|
);
|
|
1171
1265
|
} else {
|
|
1172
1266
|
/** @type {ValidationOptions} Validation behavior options */
|
|
@@ -1183,9 +1277,51 @@ export class ValidatorOptions {
|
|
|
1183
1277
|
}
|
|
1184
1278
|
}
|
|
1185
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* The object a compiled validator returns when `collectErrors` is enabled.
|
|
1282
|
+
* @typedef {{ valid: boolean, errors: import("./messages.js").ValidationError[] }} ValidationResultObject
|
|
1283
|
+
*/
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* A compiled validator in the default boolean mode. It is a type guard, so
|
|
1287
|
+
* `T` is whatever the caller asserts the schema describes; with no `T` it
|
|
1288
|
+
* behaves as an ordinary boolean predicate.
|
|
1289
|
+
* @template T
|
|
1290
|
+
* @typedef {(data: unknown) => data is T} CompiledPredicate
|
|
1291
|
+
*/
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* A compiled validator in collect-errors mode.
|
|
1295
|
+
* @typedef {(data: unknown) => ValidationResultObject} CompiledCollector
|
|
1296
|
+
*/
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* The plain-object form accepted by the JarenValidator constructor, mixing
|
|
1300
|
+
* validator-level settings with the ValidationOptions fields.
|
|
1301
|
+
* @template {boolean} [TCollect=false]
|
|
1302
|
+
* @typedef {object} ValidatorInit
|
|
1303
|
+
* @property {Record<string, FormatCompiler>} [formats] - Format compilers to register
|
|
1304
|
+
* @property {(JSONSchema | boolean)[]} [schemas] - Schemas to register
|
|
1305
|
+
* @property {ValidationOptions} [validation] - Validation behavior options
|
|
1306
|
+
* @property {TraverseOptions} [traverse] - Schema traversal options
|
|
1307
|
+
* @property {TCollect} [collectErrors] - Return `{ valid, errors }` instead of a boolean
|
|
1308
|
+
* @property {boolean} [skipErrors] - Stop at the first failure (defaults to `!collectErrors`)
|
|
1309
|
+
* @property {boolean} [useGrapheme] - Count grapheme clusters for string length
|
|
1310
|
+
* @property {boolean} [contentValidation] - Assert contentEncoding/contentMediaType
|
|
1311
|
+
* @property {number} [draftVersion] - The JSON Schema draft version
|
|
1312
|
+
* @property {boolean} [formatAssertion] - Assert the format keyword
|
|
1313
|
+
* @property {boolean} [messages] - Render English message text on collected errors
|
|
1314
|
+
* @property {'error'|'ignore'} [unknownFormats] - What an ASSERTING `format` with no registered compiler does: 'ignore' (default, per spec) accepts it as an annotation, 'error' throws at compile time
|
|
1315
|
+
*/
|
|
1316
|
+
|
|
1186
1317
|
/**
|
|
1187
1318
|
* JarenValidator is the main entry point for JSON Schema validation.
|
|
1188
1319
|
* It manages schema registration, format registration, and compilation.
|
|
1320
|
+
*
|
|
1321
|
+
* The `collectErrors` option decides what a compiled validator returns, and
|
|
1322
|
+
* it is carried in the type parameter so the two shapes never have to be
|
|
1323
|
+
* distinguished at runtime.
|
|
1324
|
+
* @template {boolean} [TCollect=false]
|
|
1189
1325
|
* @class
|
|
1190
1326
|
* @example
|
|
1191
1327
|
* const validator = new JarenValidator();
|
|
@@ -1205,7 +1341,7 @@ export class JarenValidator {
|
|
|
1205
1341
|
|
|
1206
1342
|
/**
|
|
1207
1343
|
* Creates a new JarenValidator instance.
|
|
1208
|
-
* @param {ValidatorOptions} [options] - Validator options including formats, schemas, validation options, and traverse options
|
|
1344
|
+
* @param {ValidatorOptions | ValidatorInit<TCollect>} [options] - Validator options including formats, schemas, validation options, and traverse options
|
|
1209
1345
|
*/
|
|
1210
1346
|
constructor(options = new ValidatorOptions()) {
|
|
1211
1347
|
// Accept a plain options object ({ skipErrors, collectErrors, ... })
|
|
@@ -1223,7 +1359,7 @@ export class JarenValidator {
|
|
|
1223
1359
|
* Adds a format validator.
|
|
1224
1360
|
* @param {string} name - The format name (e.g., 'email', 'uri', 'date-time')
|
|
1225
1361
|
* @param {FormatCompiler} formatCompiler - A function that compiles format validators
|
|
1226
|
-
* @returns {
|
|
1362
|
+
* @returns {this} This validator instance for chaining (the polymorphic `this` keeps the collectErrors type parameter across a chain)
|
|
1227
1363
|
* @example
|
|
1228
1364
|
* validator.addFormat('custom', (schemaObj, schema) => {
|
|
1229
1365
|
* return (data) => data.startsWith('custom:');
|
|
@@ -1240,7 +1376,7 @@ export class JarenValidator {
|
|
|
1240
1376
|
/**
|
|
1241
1377
|
* Adds multiple format validators at once.
|
|
1242
1378
|
* @param {Record<string, FormatCompiler>} formatCompilers - Object mapping format names to compiler functions
|
|
1243
|
-
* @returns {
|
|
1379
|
+
* @returns {this} This validator instance for chaining (the polymorphic `this` keeps the collectErrors type parameter across a chain)
|
|
1244
1380
|
*/
|
|
1245
1381
|
addFormats(formatCompilers) {
|
|
1246
1382
|
registerFormatCompilers(
|
|
@@ -1286,7 +1422,7 @@ export class JarenValidator {
|
|
|
1286
1422
|
* Dependencies can be added in any order, and circular dependencies are supported.
|
|
1287
1423
|
* @param {JSONSchema | boolean | (JSONSchema | boolean)[]} schema - The schema(s) to add
|
|
1288
1424
|
* @param {string} [key] - Optional key/URI to register the schema under
|
|
1289
|
-
* @returns {
|
|
1425
|
+
* @returns {this} This validator instance for chaining (the polymorphic `this` keeps the collectErrors type parameter across a chain)
|
|
1290
1426
|
* @example
|
|
1291
1427
|
* // Add a single schema
|
|
1292
1428
|
* validator.addSchema({ $id: 'http://example.com/user', type: 'object' });
|
|
@@ -1348,7 +1484,7 @@ export class JarenValidator {
|
|
|
1348
1484
|
const newSchemas = new Map();
|
|
1349
1485
|
try {
|
|
1350
1486
|
storeSchemaIdsInMap(newSchemas, baseUri, schema, scopedOpts);
|
|
1351
|
-
} catch (
|
|
1487
|
+
} catch (_e) {
|
|
1352
1488
|
// Ignore errors for already-existing schemas at the root level
|
|
1353
1489
|
}
|
|
1354
1490
|
|
|
@@ -1360,144 +1496,19 @@ export class JarenValidator {
|
|
|
1360
1496
|
}
|
|
1361
1497
|
}
|
|
1362
1498
|
|
|
1363
|
-
/**
|
|
1364
|
-
* Convert internal validation errors to public ValidationError format
|
|
1365
|
-
* @param {InternalValidationError[]} internalErrors
|
|
1366
|
-
* @returns {ValidationError[]}
|
|
1367
|
-
*/
|
|
1368
|
-
static #convertErrors(internalErrors) {
|
|
1369
|
-
return internalErrors.map(err => {
|
|
1370
|
-
const keyword = Array.isArray(err.key) ? err.key[err.key.length - 1] : err.key;
|
|
1371
|
-
|
|
1372
|
-
// Build params based on error type
|
|
1373
|
-
const params = {};
|
|
1374
|
-
if (keyword === 'required') {
|
|
1375
|
-
params.missingProperty = err.dataKey;
|
|
1376
|
-
} else if (keyword === 'type') {
|
|
1377
|
-
if (Array.isArray(err.expected)) {
|
|
1378
|
-
params.types = err.expected;
|
|
1379
|
-
} else {
|
|
1380
|
-
params.type = err.expected;
|
|
1381
|
-
}
|
|
1382
|
-
} else if (['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'minLength', 'maxLength', 'minProperties', 'maxProperties', 'minItems', 'maxItems'].includes(keyword)) {
|
|
1383
|
-
params.limit = err.expected;
|
|
1384
|
-
if (keyword === 'minimum' || keyword === 'maximum') {
|
|
1385
|
-
params.comparison = keyword === 'minimum' ? '>=' : '<=';
|
|
1386
|
-
} else if (keyword === 'exclusiveMinimum' || keyword === 'exclusiveMaximum') {
|
|
1387
|
-
params.comparison = keyword === 'exclusiveMinimum' ? '>' : '<';
|
|
1388
|
-
}
|
|
1389
|
-
} else if (keyword === 'multipleOf') {
|
|
1390
|
-
params.multipleOf = err.expected;
|
|
1391
|
-
} else if (keyword === 'pattern') {
|
|
1392
|
-
params.pattern = err.expected?.source || err.expected;
|
|
1393
|
-
} else if (keyword === 'additionalProperties') {
|
|
1394
|
-
params.additionalProperty = err.dataKey;
|
|
1395
|
-
} else if (keyword === '$query') {
|
|
1396
|
-
// A '$query' runtime failure passes the JQ2xxx code and the query
|
|
1397
|
-
// document pointer as extra meta arguments after the data path;
|
|
1398
|
-
// a plain EBV-false failure passes neither.
|
|
1399
|
-
if (err.rest != null && err.rest.length > 1) {
|
|
1400
|
-
params.code = err.rest[1];
|
|
1401
|
-
params.docPath = err.rest[2];
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
// Generate message
|
|
1406
|
-
let message = `validation failed for keyword '${keyword}'`;
|
|
1407
|
-
if (keyword === 'required') {
|
|
1408
|
-
message = params.missingProperty
|
|
1409
|
-
? `must have required property '${params.missingProperty}'`
|
|
1410
|
-
: 'must have required properties';
|
|
1411
|
-
} else if (keyword === 'type') {
|
|
1412
|
-
message = params.types
|
|
1413
|
-
? `must be one of the following types: ${params.types.join(', ')}`
|
|
1414
|
-
: `must be ${params.type === 'integer' ? 'an' : 'a'} ${params.type}`;
|
|
1415
|
-
} else if (keyword === 'minimum' || keyword === 'maximum') {
|
|
1416
|
-
message = `must be ${params.comparison} ${params.limit}`;
|
|
1417
|
-
} else if (keyword === 'exclusiveMinimum' || keyword === 'exclusiveMaximum') {
|
|
1418
|
-
message = `must be ${params.comparison} ${params.limit}`;
|
|
1419
|
-
} else if (keyword === 'multipleOf') {
|
|
1420
|
-
message = `must be multiple of ${params.multipleOf}`;
|
|
1421
|
-
} else if (keyword === 'minLength') {
|
|
1422
|
-
message = `must NOT have fewer than ${params.limit} characters`;
|
|
1423
|
-
} else if (keyword === 'maxLength') {
|
|
1424
|
-
message = `must NOT have more than ${params.limit} characters`;
|
|
1425
|
-
} else if (keyword === 'pattern') {
|
|
1426
|
-
message = `must match pattern "${params.pattern}"`;
|
|
1427
|
-
} else if (keyword === 'additionalProperties') {
|
|
1428
|
-
message = params.additionalProperty
|
|
1429
|
-
? `must NOT have additional property '${params.additionalProperty}'`
|
|
1430
|
-
: 'must NOT have additional properties';
|
|
1431
|
-
} else if (keyword === 'minProperties') {
|
|
1432
|
-
message = `must NOT have fewer than ${params.limit} properties`;
|
|
1433
|
-
} else if (keyword === 'maxProperties') {
|
|
1434
|
-
message = `must NOT have more than ${params.limit} properties`;
|
|
1435
|
-
} else if (keyword === 'minItems') {
|
|
1436
|
-
message = `must NOT have fewer than ${params.limit} items`;
|
|
1437
|
-
} else if (keyword === 'maxItems') {
|
|
1438
|
-
message = `must NOT have more than ${params.limit} items`;
|
|
1439
|
-
} else if (keyword === 'uniqueItems') {
|
|
1440
|
-
message = 'must NOT have duplicate items';
|
|
1441
|
-
} else if (keyword === 'contains') {
|
|
1442
|
-
message = 'must contain at least one valid item';
|
|
1443
|
-
} else if (keyword === 'items') {
|
|
1444
|
-
message = 'array items are invalid';
|
|
1445
|
-
} else if (keyword === 'allOf') {
|
|
1446
|
-
message = 'must match all of the subschemas';
|
|
1447
|
-
} else if (keyword === 'anyOf') {
|
|
1448
|
-
message = 'must match a subschema in anyOf';
|
|
1449
|
-
} else if (keyword === 'oneOf') {
|
|
1450
|
-
message = 'must match exactly one subschema in oneOf';
|
|
1451
|
-
} else if (keyword === 'not') {
|
|
1452
|
-
message = 'must NOT match the subschema';
|
|
1453
|
-
} else if (keyword === 'format') {
|
|
1454
|
-
const formatName = err.expected || err.value;
|
|
1455
|
-
params.format = formatName;
|
|
1456
|
-
message = `must match format "${formatName}"`;
|
|
1457
|
-
} else if (keyword === 'if') {
|
|
1458
|
-
message = 'must match "if" schema';
|
|
1459
|
-
} else if (keyword === 'then') {
|
|
1460
|
-
message = 'must match "then" schema';
|
|
1461
|
-
} else if (keyword === 'else') {
|
|
1462
|
-
message = 'must match "else" schema';
|
|
1463
|
-
} else if (keyword === 'false schema') {
|
|
1464
|
-
message = 'boolean schema false is always invalid';
|
|
1465
|
-
} else if (keyword === '$query') {
|
|
1466
|
-
message = params.code
|
|
1467
|
-
? `'$query' assertion raised ${params.code} at '${params.docPath}'`
|
|
1468
|
-
: "must satisfy the '$query' assertion";
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// Validators pass the data path as the first meta argument to the
|
|
1472
|
-
// error handler; use it when it looks like a JSON pointer.
|
|
1473
|
-
const meta0 = err.rest?.[0];
|
|
1474
|
-
const instancePath = (typeof meta0 === 'string' && (meta0 === '' || meta0.charCodeAt(0) === 0x2f))
|
|
1475
|
-
? meta0
|
|
1476
|
-
: '';
|
|
1477
|
-
|
|
1478
|
-
return new ValidationError({
|
|
1479
|
-
keyword,
|
|
1480
|
-
instancePath,
|
|
1481
|
-
schemaPath: err.object?.path || '',
|
|
1482
|
-
params,
|
|
1483
|
-
message,
|
|
1484
|
-
});
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
1499
|
/**
|
|
1489
1500
|
*
|
|
1490
1501
|
* @param {JarenValidator} self
|
|
1491
1502
|
* @param {string} origin
|
|
1492
1503
|
* @param {Map} schemas
|
|
1493
|
-
* @returns {(data) => boolean | {valid: boolean, errors: ValidationError[]}}
|
|
1504
|
+
* @returns {(data) => boolean | {valid: boolean, errors: import("./messages.js").ValidationError[]}}
|
|
1494
1505
|
*/
|
|
1495
|
-
static #compileSchema(self, origin, schemas) {
|
|
1506
|
+
static #compileSchema(self, origin, schemas, validation = self.#options.validation) {
|
|
1496
1507
|
const root = new ValidationRoot(
|
|
1497
1508
|
origin,
|
|
1498
1509
|
schemas,
|
|
1499
1510
|
self.#formats,
|
|
1500
|
-
|
|
1511
|
+
validation,
|
|
1501
1512
|
self.#options.traverse,
|
|
1502
1513
|
self);
|
|
1503
1514
|
|
|
@@ -1508,16 +1519,12 @@ export class JarenValidator {
|
|
|
1508
1519
|
if (collectErrors) {
|
|
1509
1520
|
return {
|
|
1510
1521
|
valid,
|
|
1511
|
-
errors: valid ? [] :
|
|
1522
|
+
errors: valid ? [] : convertInternalErrors(root.errors)
|
|
1512
1523
|
};
|
|
1513
1524
|
}
|
|
1514
1525
|
return valid;
|
|
1515
1526
|
}
|
|
1516
1527
|
|
|
1517
|
-
Object.defineProperty(jarenValidateSchema, "errors", {
|
|
1518
|
-
get: function () { return root.errors }
|
|
1519
|
-
})
|
|
1520
|
-
|
|
1521
1528
|
return jarenValidateSchema;
|
|
1522
1529
|
}
|
|
1523
1530
|
|
|
@@ -1527,20 +1534,25 @@ export class JarenValidator {
|
|
|
1527
1534
|
* @param {string} origin
|
|
1528
1535
|
* @param {Map} schemas
|
|
1529
1536
|
* @param {ValidationRoot} root - Pre-created root with pre-compiled refs
|
|
1530
|
-
* @returns {(data) => boolean | {valid: boolean, errors: ValidationError[]}}
|
|
1537
|
+
* @returns {(data) => boolean | {valid: boolean, errors: import("./messages.js").ValidationError[]}}
|
|
1531
1538
|
*/
|
|
1532
1539
|
static #compileSchemaWithRoot(self, origin, schemas, root) {
|
|
1533
1540
|
const collectErrors = self.#options.validation?.collectErrors || false;
|
|
1534
1541
|
|
|
1542
|
+
if (!collectErrors) {
|
|
1543
|
+
const jarenValidateSchema = root.createValidateFn();
|
|
1544
|
+
Object.defineProperty(jarenValidateSchema, "errors", {
|
|
1545
|
+
get: function () { return root.errors }
|
|
1546
|
+
})
|
|
1547
|
+
return jarenValidateSchema;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1535
1550
|
function jarenValidateSchema(data) {
|
|
1536
1551
|
const valid = root.validate(data);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
return valid;
|
|
1552
|
+
return {
|
|
1553
|
+
valid,
|
|
1554
|
+
errors: valid ? [] : convertInternalErrors(root.errors)
|
|
1555
|
+
};
|
|
1544
1556
|
}
|
|
1545
1557
|
|
|
1546
1558
|
Object.defineProperty(jarenValidateSchema, "errors", {
|
|
@@ -1559,28 +1571,45 @@ export class JarenValidator {
|
|
|
1559
1571
|
* Meta-schemas are schemas that describe the structure of valid JSON schemas.
|
|
1560
1572
|
* @param {JSONSchema | boolean | (JSONSchema | boolean)[]} schema - The meta-schema(s) to add
|
|
1561
1573
|
* @param {string} [key] - Optional key/URI for the meta-schema
|
|
1562
|
-
* @returns {
|
|
1574
|
+
* @returns {this} This validator instance for chaining (the polymorphic `this` keeps the collectErrors type parameter across a chain)
|
|
1563
1575
|
* @example
|
|
1564
1576
|
* validator.addMetaSchema(draft7MetaSchema, 'http://json-schema.org/draft-07/schema');
|
|
1565
1577
|
*/
|
|
1566
1578
|
addMetaSchema(schema, key = undefined) {
|
|
1567
1579
|
key = JarenValidator.normalizeUriKey(key)
|
|
1580
|
+
// A meta-schema is infrastructure, not something the caller authored:
|
|
1581
|
+
// every JSON Schema meta-schema declares `format: "uri-reference"` on
|
|
1582
|
+
// `$id`/`$ref`, and nobody registers formats in order to check that a
|
|
1583
|
+
// SCHEMA is well-formed. The unknownFormats guard protects an author
|
|
1584
|
+
// from a keyword of their own that silently checks nothing, so it does
|
|
1585
|
+
// not apply here — otherwise merely registering draft-07 would throw.
|
|
1586
|
+
const validation = JarenValidator.#withOption(this.#options.validation,
|
|
1587
|
+
'unknownFormats', 'ignore');
|
|
1568
1588
|
if (Array.isArray(schema)) {
|
|
1569
1589
|
const first = schema.shift();
|
|
1570
1590
|
const { origin, map } = JarenValidator.#traverseSchema(first, schema, undefined, new TraverseOptions(key));
|
|
1571
|
-
const compiled = JarenValidator.#compileSchema(this, origin, map);
|
|
1591
|
+
const compiled = JarenValidator.#compileSchema(this, origin, map, validation);
|
|
1572
1592
|
this.#metaSchemas.set(origin, compiled);
|
|
1573
1593
|
mergeMap(this.#schemas, map);
|
|
1574
1594
|
}
|
|
1575
1595
|
else if (isBoolOrObjectClass(schema)) {
|
|
1576
1596
|
const { origin, map } = JarenValidator.#traverseSchema(schema, undefined, undefined, new TraverseOptions(key));
|
|
1577
|
-
const compiled = JarenValidator.#compileSchema(this, origin, map);
|
|
1597
|
+
const compiled = JarenValidator.#compileSchema(this, origin, map, validation);
|
|
1578
1598
|
this.#metaSchemas.set(origin, compiled);
|
|
1579
1599
|
mergeMap(this.#schemas, map);
|
|
1580
1600
|
}
|
|
1581
1601
|
return this;
|
|
1582
1602
|
}
|
|
1583
1603
|
|
|
1604
|
+
/** A ValidationOptions with one member replaced, leaving the original
|
|
1605
|
+
* untouched (the validator's own options must not drift). */
|
|
1606
|
+
static #withOption(validation, name, value) {
|
|
1607
|
+
const next = new ValidationOptions();
|
|
1608
|
+
Object.assign(next, validation ?? new ValidationOptions());
|
|
1609
|
+
next[name] = value;
|
|
1610
|
+
return next;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1584
1613
|
/**
|
|
1585
1614
|
* Retrieves a registered schema by its key/URI.
|
|
1586
1615
|
* @param {string} key - The schema URI/key
|
|
@@ -1645,7 +1674,7 @@ export class JarenValidator {
|
|
|
1645
1674
|
// This is a canonical $id path - create with origin as base
|
|
1646
1675
|
try {
|
|
1647
1676
|
root.createObject(id, schema, origin);
|
|
1648
|
-
} catch (
|
|
1677
|
+
} catch (_e) {
|
|
1649
1678
|
// May fail if dependencies not resolved yet
|
|
1650
1679
|
}
|
|
1651
1680
|
}
|
|
@@ -1751,7 +1780,7 @@ export class JarenValidator {
|
|
|
1751
1780
|
baseUri = candidateBase;
|
|
1752
1781
|
break;
|
|
1753
1782
|
}
|
|
1754
|
-
} catch (
|
|
1783
|
+
} catch (_e) {
|
|
1755
1784
|
// Invalid URL, skip this candidate
|
|
1756
1785
|
}
|
|
1757
1786
|
}
|
|
@@ -1760,7 +1789,7 @@ export class JarenValidator {
|
|
|
1760
1789
|
|
|
1761
1790
|
try {
|
|
1762
1791
|
root.createObject(id, schema, baseUri);
|
|
1763
|
-
} catch (
|
|
1792
|
+
} catch (_e) {
|
|
1764
1793
|
// Ref may not be resolvable yet, that's ok
|
|
1765
1794
|
}
|
|
1766
1795
|
}
|
|
@@ -1770,9 +1799,16 @@ export class JarenValidator {
|
|
|
1770
1799
|
* Compiles a schema into a validation function.
|
|
1771
1800
|
* This is the main method for creating validators. It resolves all $ref references,
|
|
1772
1801
|
* compiles the schema structure, and returns a function that validates data.
|
|
1802
|
+
* The return type follows the instance's `collectErrors` setting: a type
|
|
1803
|
+
* guard over `unknown` by default, or a function producing
|
|
1804
|
+
* `{ valid, errors }` when errors are collected. Jaren does not infer `T`
|
|
1805
|
+
* from the schema — the caller asserts what the schema describes, which is
|
|
1806
|
+
* what a checked contract wrapper wants; pair it with a schema-to-type
|
|
1807
|
+
* generator if you need the shape derived mechanically.
|
|
1808
|
+
* @template [T=unknown]
|
|
1773
1809
|
* @param {JSONSchema | boolean} schema - The schema to compile
|
|
1774
1810
|
* @param {(JSONSchema | boolean)[]} [schemas] - Additional schemas to reference during compilation
|
|
1775
|
-
* @returns {
|
|
1811
|
+
* @returns {TCollect extends true ? CompiledCollector : CompiledPredicate<T>} A validation function
|
|
1776
1812
|
* @example
|
|
1777
1813
|
* const validate = validator.compile({
|
|
1778
1814
|
* type: 'object',
|
|
@@ -1784,9 +1820,13 @@ export class JarenValidator {
|
|
|
1784
1820
|
* const valid = validate({ name: 'John' }); // true
|
|
1785
1821
|
* const invalid = validate({ name: 123 }); // false
|
|
1786
1822
|
*
|
|
1823
|
+
* // Narrowing to a caller-asserted type
|
|
1824
|
+
* const isUser = validator.compile<{ name: string }>(userSchema);
|
|
1825
|
+
* if (isUser(input)) input.name; // input is { name: string } here
|
|
1826
|
+
*
|
|
1787
1827
|
* // With error collection
|
|
1788
|
-
*
|
|
1789
|
-
* const result =
|
|
1828
|
+
* const collecting = new JarenValidator({ collectErrors: true });
|
|
1829
|
+
* const result = collecting.compile(schema)({ name: 123 });
|
|
1790
1830
|
* // result = { valid: false, errors: [...] }
|
|
1791
1831
|
*/
|
|
1792
1832
|
compile(schema, schemas = undefined) {
|
|
@@ -1831,7 +1871,9 @@ export class JarenValidator {
|
|
|
1831
1871
|
existingValidation.contentValidation ?? contentValidationDefault,
|
|
1832
1872
|
draftVersion,
|
|
1833
1873
|
vocabValidation,
|
|
1834
|
-
formatAssertion
|
|
1874
|
+
formatAssertion,
|
|
1875
|
+
existingValidation.messages ?? true,
|
|
1876
|
+
existingValidation.unknownFormats ?? 'ignore'
|
|
1835
1877
|
);
|
|
1836
1878
|
|
|
1837
1879
|
// Pre-compile all refs before returning the validator
|