@tldraw/validate 4.2.2 → 4.2.3
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/dist-cjs/index.d.ts +1 -37
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/validation.js +68 -304
- package/dist-cjs/lib/validation.js.map +3 -3
- package/dist-esm/index.d.mts +1 -37
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/validation.mjs +68 -304
- package/dist-esm/lib/validation.mjs.map +3 -3
- package/package.json +2 -2
- package/src/lib/validation.ts +68 -355
- package/src/test/validation.test.ts +5 -38
package/src/lib/validation.ts
CHANGED
|
@@ -9,9 +9,6 @@ import {
|
|
|
9
9
|
validateIndexKey,
|
|
10
10
|
} from '@tldraw/utils'
|
|
11
11
|
|
|
12
|
-
/** @internal */
|
|
13
|
-
const IS_DEV = process.env.NODE_ENV !== 'production'
|
|
14
|
-
|
|
15
12
|
/**
|
|
16
13
|
* A function that validates and returns a value of type T from unknown input.
|
|
17
14
|
* The function should throw a ValidationError if the value is invalid.
|
|
@@ -234,13 +231,10 @@ export class Validator<T> implements Validatable<T> {
|
|
|
234
231
|
*
|
|
235
232
|
* validationFn - Function that validates and returns a value of type T
|
|
236
233
|
* validateUsingKnownGoodVersionFn - Optional performance-optimized validation function
|
|
237
|
-
* skipSameValueCheck - Internal flag to skip dev check for validators that transform values
|
|
238
234
|
*/
|
|
239
235
|
constructor(
|
|
240
236
|
readonly validationFn: ValidatorFn<T>,
|
|
241
|
-
readonly validateUsingKnownGoodVersionFn?: ValidatorUsingKnownGoodVersionFn<T
|
|
242
|
-
/** @internal */
|
|
243
|
-
readonly skipSameValueCheck: boolean = false
|
|
237
|
+
readonly validateUsingKnownGoodVersionFn?: ValidatorUsingKnownGoodVersionFn<T>
|
|
244
238
|
) {}
|
|
245
239
|
|
|
246
240
|
/**
|
|
@@ -265,7 +259,7 @@ export class Validator<T> implements Validatable<T> {
|
|
|
265
259
|
*/
|
|
266
260
|
validate(value: unknown): T {
|
|
267
261
|
const validated = this.validationFn(value)
|
|
268
|
-
if (
|
|
262
|
+
if (process.env.NODE_ENV !== 'production' && !Object.is(value, validated)) {
|
|
269
263
|
throw new ValidationError('Validator functions must return the same value they were passed')
|
|
270
264
|
}
|
|
271
265
|
return validated
|
|
@@ -434,8 +428,7 @@ export class Validator<T> implements Validatable<T> {
|
|
|
434
428
|
return knownGoodValue
|
|
435
429
|
}
|
|
436
430
|
return otherValidationFn(validated)
|
|
437
|
-
}
|
|
438
|
-
true // skipSameValueCheck: refine is designed to transform values
|
|
431
|
+
}
|
|
439
432
|
)
|
|
440
433
|
}
|
|
441
434
|
|
|
@@ -523,27 +516,11 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
|
|
|
523
516
|
(value) => {
|
|
524
517
|
const arr = array.validate(value)
|
|
525
518
|
for (let i = 0; i < arr.length; i++) {
|
|
526
|
-
|
|
527
|
-
prefixError(i, () => itemValidator.validate(arr[i]))
|
|
528
|
-
} else {
|
|
529
|
-
// Production: inline error handling to avoid closure overhead
|
|
530
|
-
try {
|
|
531
|
-
itemValidator.validate(arr[i])
|
|
532
|
-
} catch (err) {
|
|
533
|
-
if (err instanceof ValidationError) {
|
|
534
|
-
throw new ValidationError(err.rawMessage, [i, ...err.path])
|
|
535
|
-
}
|
|
536
|
-
throw new ValidationError((err as Error).toString(), [i])
|
|
537
|
-
}
|
|
538
|
-
}
|
|
519
|
+
prefixError(i, () => itemValidator.validate(arr[i]))
|
|
539
520
|
}
|
|
540
521
|
return arr as T[]
|
|
541
522
|
},
|
|
542
523
|
(knownGoodValue, newValue) => {
|
|
543
|
-
// Fast path: reference equality means no changes
|
|
544
|
-
if (Object.is(knownGoodValue, newValue)) {
|
|
545
|
-
return knownGoodValue
|
|
546
|
-
}
|
|
547
524
|
if (!itemValidator.validateUsingKnownGoodVersion) return this.validate(newValue)
|
|
548
525
|
const arr = array.validate(newValue)
|
|
549
526
|
let isDifferent = knownGoodValue.length !== arr.length
|
|
@@ -551,46 +528,18 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
|
|
|
551
528
|
const item = arr[i]
|
|
552
529
|
if (i >= knownGoodValue.length) {
|
|
553
530
|
isDifferent = true
|
|
554
|
-
|
|
555
|
-
prefixError(i, () => itemValidator.validate(item))
|
|
556
|
-
} else {
|
|
557
|
-
try {
|
|
558
|
-
itemValidator.validate(item)
|
|
559
|
-
} catch (err) {
|
|
560
|
-
if (err instanceof ValidationError) {
|
|
561
|
-
throw new ValidationError(err.rawMessage, [i, ...err.path])
|
|
562
|
-
}
|
|
563
|
-
throw new ValidationError((err as Error).toString(), [i])
|
|
564
|
-
}
|
|
565
|
-
}
|
|
531
|
+
prefixError(i, () => itemValidator.validate(item))
|
|
566
532
|
continue
|
|
567
533
|
}
|
|
568
534
|
// sneaky quick check here to avoid the prefix + validator overhead
|
|
569
535
|
if (Object.is(knownGoodValue[i], item)) {
|
|
570
536
|
continue
|
|
571
537
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
isDifferent = true
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
try {
|
|
581
|
-
const checkedItem = itemValidator.validateUsingKnownGoodVersion!(
|
|
582
|
-
knownGoodValue[i],
|
|
583
|
-
item
|
|
584
|
-
)
|
|
585
|
-
if (!Object.is(checkedItem, knownGoodValue[i])) {
|
|
586
|
-
isDifferent = true
|
|
587
|
-
}
|
|
588
|
-
} catch (err) {
|
|
589
|
-
if (err instanceof ValidationError) {
|
|
590
|
-
throw new ValidationError(err.rawMessage, [i, ...err.path])
|
|
591
|
-
}
|
|
592
|
-
throw new ValidationError((err as Error).toString(), [i])
|
|
593
|
-
}
|
|
538
|
+
const checkedItem = prefixError(i, () =>
|
|
539
|
+
itemValidator.validateUsingKnownGoodVersion!(knownGoodValue[i], item)
|
|
540
|
+
)
|
|
541
|
+
if (!Object.is(checkedItem, knownGoodValue[i])) {
|
|
542
|
+
isDifferent = true
|
|
594
543
|
}
|
|
595
544
|
}
|
|
596
545
|
|
|
@@ -679,24 +628,10 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|
|
679
628
|
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
|
680
629
|
}
|
|
681
630
|
|
|
682
|
-
for (const key
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
prefixError(key, () => {
|
|
687
|
-
;(validator as Validatable<unknown>).validate(getOwnProperty(object, key))
|
|
688
|
-
})
|
|
689
|
-
} else {
|
|
690
|
-
// Production: inline error handling to avoid closure overhead
|
|
691
|
-
try {
|
|
692
|
-
;(validator as Validatable<unknown>).validate(getOwnProperty(object, key))
|
|
693
|
-
} catch (err) {
|
|
694
|
-
if (err instanceof ValidationError) {
|
|
695
|
-
throw new ValidationError(err.rawMessage, [key, ...err.path])
|
|
696
|
-
}
|
|
697
|
-
throw new ValidationError((err as Error).toString(), [key])
|
|
698
|
-
}
|
|
699
|
-
}
|
|
631
|
+
for (const [key, validator] of Object.entries(config)) {
|
|
632
|
+
prefixError(key, () => {
|
|
633
|
+
;(validator as Validatable<unknown>).validate(getOwnProperty(object, key))
|
|
634
|
+
})
|
|
700
635
|
}
|
|
701
636
|
|
|
702
637
|
if (!shouldAllowUnknownProperties) {
|
|
@@ -710,52 +645,29 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|
|
710
645
|
return object as Shape
|
|
711
646
|
},
|
|
712
647
|
(knownGoodValue, newValue) => {
|
|
713
|
-
// Fast path: reference equality means no changes
|
|
714
|
-
if (Object.is(knownGoodValue, newValue)) {
|
|
715
|
-
return knownGoodValue
|
|
716
|
-
}
|
|
717
648
|
if (typeof newValue !== 'object' || newValue === null) {
|
|
718
649
|
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
|
|
719
650
|
}
|
|
720
651
|
|
|
721
652
|
let isDifferent = false
|
|
722
653
|
|
|
723
|
-
for (const key
|
|
724
|
-
if (!hasOwnProperty(config, key)) continue
|
|
725
|
-
const validator = config[key as keyof typeof config]
|
|
654
|
+
for (const [key, validator] of Object.entries(config)) {
|
|
726
655
|
const prev = getOwnProperty(knownGoodValue, key)
|
|
727
656
|
const next = getOwnProperty(newValue, key)
|
|
728
657
|
// sneaky quick check here to avoid the prefix + validator overhead
|
|
729
658
|
if (Object.is(prev, next)) {
|
|
730
659
|
continue
|
|
731
660
|
}
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return validatable.validate(next)
|
|
739
|
-
}
|
|
740
|
-
})
|
|
741
|
-
if (!Object.is(checked, prev)) {
|
|
742
|
-
isDifferent = true
|
|
743
|
-
}
|
|
744
|
-
} else {
|
|
745
|
-
try {
|
|
746
|
-
const validatable = validator as Validatable<unknown>
|
|
747
|
-
const checked = validatable.validateUsingKnownGoodVersion
|
|
748
|
-
? validatable.validateUsingKnownGoodVersion(prev, next)
|
|
749
|
-
: validatable.validate(next)
|
|
750
|
-
if (!Object.is(checked, prev)) {
|
|
751
|
-
isDifferent = true
|
|
752
|
-
}
|
|
753
|
-
} catch (err) {
|
|
754
|
-
if (err instanceof ValidationError) {
|
|
755
|
-
throw new ValidationError(err.rawMessage, [key, ...err.path])
|
|
756
|
-
}
|
|
757
|
-
throw new ValidationError((err as Error).toString(), [key])
|
|
661
|
+
const checked = prefixError(key, () => {
|
|
662
|
+
const validatable = validator as Validatable<unknown>
|
|
663
|
+
if (validatable.validateUsingKnownGoodVersion) {
|
|
664
|
+
return validatable.validateUsingKnownGoodVersion(prev, next)
|
|
665
|
+
} else {
|
|
666
|
+
return validatable.validate(next)
|
|
758
667
|
}
|
|
668
|
+
})
|
|
669
|
+
if (!Object.is(checked, prev)) {
|
|
670
|
+
isDifferent = true
|
|
759
671
|
}
|
|
760
672
|
}
|
|
761
673
|
|
|
@@ -882,7 +794,6 @@ export class UnionValidator<
|
|
|
882
794
|
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(input))
|
|
883
795
|
},
|
|
884
796
|
(prevValue, newValue) => {
|
|
885
|
-
// Note: Object.is check is already done by base Validator class
|
|
886
797
|
this.expectObject(newValue)
|
|
887
798
|
this.expectObject(prevValue)
|
|
888
799
|
|
|
@@ -922,15 +833,8 @@ export class UnionValidator<
|
|
|
922
833
|
throw new ValidationError(
|
|
923
834
|
`Expected a string for key "${this.key}", got ${typeToString(variant)}`
|
|
924
835
|
)
|
|
925
|
-
} else if (this.useNumberKeys) {
|
|
926
|
-
|
|
927
|
-
// This avoids Number.isFinite function call overhead
|
|
928
|
-
const numVariant = Number(variant)
|
|
929
|
-
if (numVariant - numVariant !== 0) {
|
|
930
|
-
throw new ValidationError(
|
|
931
|
-
`Expected a number for key "${this.key}", got "${variant as any}"`
|
|
932
|
-
)
|
|
933
|
-
}
|
|
836
|
+
} else if (this.useNumberKeys && !Number.isFinite(Number(variant))) {
|
|
837
|
+
throw new ValidationError(`Expected a number for key "${this.key}", got "${variant as any}"`)
|
|
934
838
|
}
|
|
935
839
|
|
|
936
840
|
const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined
|
|
@@ -991,26 +895,11 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
|
|
|
991
895
|
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
|
992
896
|
}
|
|
993
897
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
keyValidator.validate(key)
|
|
1000
|
-
valueValidator.validate((object as Record<string, unknown>)[key])
|
|
1001
|
-
})
|
|
1002
|
-
} else {
|
|
1003
|
-
// Production: inline error handling to avoid closure overhead
|
|
1004
|
-
try {
|
|
1005
|
-
keyValidator.validate(key)
|
|
1006
|
-
valueValidator.validate((object as Record<string, unknown>)[key])
|
|
1007
|
-
} catch (err) {
|
|
1008
|
-
if (err instanceof ValidationError) {
|
|
1009
|
-
throw new ValidationError(err.rawMessage, [key, ...err.path])
|
|
1010
|
-
}
|
|
1011
|
-
throw new ValidationError((err as Error).toString(), [key])
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
898
|
+
for (const [key, value] of Object.entries(object)) {
|
|
899
|
+
prefixError(key, () => {
|
|
900
|
+
keyValidator.validate(key)
|
|
901
|
+
valueValidator.validate(value)
|
|
902
|
+
})
|
|
1014
903
|
}
|
|
1015
904
|
|
|
1016
905
|
return object as Record<Key, Value>
|
|
@@ -1020,84 +909,39 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
|
|
|
1020
909
|
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
|
|
1021
910
|
}
|
|
1022
911
|
|
|
1023
|
-
const newObj = newValue as Record<string, unknown>
|
|
1024
912
|
let isDifferent = false
|
|
1025
|
-
let newKeyCount = 0
|
|
1026
|
-
|
|
1027
|
-
// Use for...in instead of Object.entries() to avoid array allocation
|
|
1028
|
-
for (const key in newObj) {
|
|
1029
|
-
if (!hasOwnProperty(newObj, key)) continue
|
|
1030
|
-
newKeyCount++
|
|
1031
|
-
|
|
1032
|
-
const next = newObj[key]
|
|
1033
913
|
|
|
914
|
+
for (const [key, value] of Object.entries(newValue)) {
|
|
1034
915
|
if (!hasOwnProperty(knownGoodValue, key)) {
|
|
1035
916
|
isDifferent = true
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
})
|
|
1041
|
-
} else {
|
|
1042
|
-
try {
|
|
1043
|
-
keyValidator.validate(key)
|
|
1044
|
-
valueValidator.validate(next)
|
|
1045
|
-
} catch (err) {
|
|
1046
|
-
if (err instanceof ValidationError) {
|
|
1047
|
-
throw new ValidationError(err.rawMessage, [key, ...err.path])
|
|
1048
|
-
}
|
|
1049
|
-
throw new ValidationError((err as Error).toString(), [key])
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
917
|
+
prefixError(key, () => {
|
|
918
|
+
keyValidator.validate(key)
|
|
919
|
+
valueValidator.validate(value)
|
|
920
|
+
})
|
|
1052
921
|
continue
|
|
1053
922
|
}
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
// Quick reference equality check to avoid validator overhead
|
|
923
|
+
const prev = getOwnProperty(knownGoodValue, key)
|
|
924
|
+
const next = value
|
|
925
|
+
// sneaky quick check here to avoid the prefix + validator overhead
|
|
1058
926
|
if (Object.is(prev, next)) {
|
|
1059
927
|
continue
|
|
1060
928
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
} else {
|
|
1067
|
-
return valueValidator.validate(next)
|
|
1068
|
-
}
|
|
1069
|
-
})
|
|
1070
|
-
if (!Object.is(checked, prev)) {
|
|
1071
|
-
isDifferent = true
|
|
1072
|
-
}
|
|
1073
|
-
} else {
|
|
1074
|
-
try {
|
|
1075
|
-
const checked = valueValidator.validateUsingKnownGoodVersion
|
|
1076
|
-
? valueValidator.validateUsingKnownGoodVersion(prev as Value, next)
|
|
1077
|
-
: valueValidator.validate(next)
|
|
1078
|
-
if (!Object.is(checked, prev)) {
|
|
1079
|
-
isDifferent = true
|
|
1080
|
-
}
|
|
1081
|
-
} catch (err) {
|
|
1082
|
-
if (err instanceof ValidationError) {
|
|
1083
|
-
throw new ValidationError(err.rawMessage, [key, ...err.path])
|
|
1084
|
-
}
|
|
1085
|
-
throw new ValidationError((err as Error).toString(), [key])
|
|
929
|
+
const checked = prefixError(key, () => {
|
|
930
|
+
if (valueValidator.validateUsingKnownGoodVersion) {
|
|
931
|
+
return valueValidator.validateUsingKnownGoodVersion(prev as any, next)
|
|
932
|
+
} else {
|
|
933
|
+
return valueValidator.validate(next)
|
|
1086
934
|
}
|
|
935
|
+
})
|
|
936
|
+
if (!Object.is(checked, prev)) {
|
|
937
|
+
isDifferent = true
|
|
1087
938
|
}
|
|
1088
939
|
}
|
|
1089
940
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (!isDifferent) {
|
|
1093
|
-
let oldKeyCount = 0
|
|
1094
|
-
for (const key in knownGoodValue) {
|
|
1095
|
-
if (hasOwnProperty(knownGoodValue, key)) {
|
|
1096
|
-
oldKeyCount++
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
if (oldKeyCount !== newKeyCount) {
|
|
941
|
+
for (const key of Object.keys(knownGoodValue)) {
|
|
942
|
+
if (!hasOwnProperty(newValue, key)) {
|
|
1100
943
|
isDifferent = true
|
|
944
|
+
break
|
|
1101
945
|
}
|
|
1102
946
|
}
|
|
1103
947
|
|
|
@@ -1164,21 +1008,13 @@ export const string = typeofValidator<string>('string')
|
|
|
1164
1008
|
* ```
|
|
1165
1009
|
* @public
|
|
1166
1010
|
*/
|
|
1167
|
-
export const number =
|
|
1168
|
-
|
|
1169
|
-
// value - value === 0 is false for Infinity and NaN (avoids function call overhead)
|
|
1170
|
-
if (Number.isFinite(value)) {
|
|
1171
|
-
return value as number
|
|
1172
|
-
}
|
|
1173
|
-
// Slow path: determine specific error
|
|
1174
|
-
if (typeof value !== 'number') {
|
|
1175
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1176
|
-
}
|
|
1177
|
-
// value !== value is true only for NaN (faster than Number.isNaN)
|
|
1178
|
-
if (value !== value) {
|
|
1011
|
+
export const number = typeofValidator<number>('number').check((number) => {
|
|
1012
|
+
if (Number.isNaN(number)) {
|
|
1179
1013
|
throw new ValidationError('Expected a number, got NaN')
|
|
1180
1014
|
}
|
|
1181
|
-
|
|
1015
|
+
if (!Number.isFinite(number)) {
|
|
1016
|
+
throw new ValidationError(`Expected a finite number, got ${number}`)
|
|
1017
|
+
}
|
|
1182
1018
|
})
|
|
1183
1019
|
/**
|
|
1184
1020
|
* Validator that ensures a value is a non-negative number (\>= 0).
|
|
@@ -1192,20 +1028,8 @@ export const number = new Validator<number>((value) => {
|
|
|
1192
1028
|
* ```
|
|
1193
1029
|
* @public
|
|
1194
1030
|
*/
|
|
1195
|
-
export const positiveNumber =
|
|
1196
|
-
if (
|
|
1197
|
-
return value as number
|
|
1198
|
-
}
|
|
1199
|
-
if (typeof value !== 'number') {
|
|
1200
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1201
|
-
}
|
|
1202
|
-
if (value !== value) {
|
|
1203
|
-
throw new ValidationError('Expected a number, got NaN')
|
|
1204
|
-
}
|
|
1205
|
-
if (value < 0) {
|
|
1206
|
-
throw new ValidationError(`Expected a positive number, got ${value}`)
|
|
1207
|
-
}
|
|
1208
|
-
throw new ValidationError(`Expected a finite number, got ${value}`)
|
|
1031
|
+
export const positiveNumber = number.check((value) => {
|
|
1032
|
+
if (value < 0) throw new ValidationError(`Expected a positive number, got ${value}`)
|
|
1209
1033
|
})
|
|
1210
1034
|
/**
|
|
1211
1035
|
* Validator that ensures a value is a positive number (\> 0). Rejects zero and negative numbers.
|
|
@@ -1218,73 +1042,8 @@ export const positiveNumber = new Validator<number>((value) => {
|
|
|
1218
1042
|
* ```
|
|
1219
1043
|
* @public
|
|
1220
1044
|
*/
|
|
1221
|
-
export const nonZeroNumber =
|
|
1222
|
-
if (
|
|
1223
|
-
return value as number
|
|
1224
|
-
}
|
|
1225
|
-
if (typeof value !== 'number') {
|
|
1226
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1227
|
-
}
|
|
1228
|
-
if (value !== value) {
|
|
1229
|
-
throw new ValidationError('Expected a number, got NaN')
|
|
1230
|
-
}
|
|
1231
|
-
if (value <= 0) {
|
|
1232
|
-
throw new ValidationError(`Expected a non-zero positive number, got ${value}`)
|
|
1233
|
-
}
|
|
1234
|
-
throw new ValidationError(`Expected a finite number, got ${value}`)
|
|
1235
|
-
})
|
|
1236
|
-
/**
|
|
1237
|
-
* Validator that ensures a value is a finite, non-zero number. Allows negative numbers.
|
|
1238
|
-
* Useful for scale factors that can be negative (for flipping) but not zero.
|
|
1239
|
-
*
|
|
1240
|
-
* @example
|
|
1241
|
-
* ```ts
|
|
1242
|
-
* const scale = T.nonZeroFiniteNumber.validate(-1.5) // Returns -1.5 (valid, allows negative)
|
|
1243
|
-
* T.nonZeroFiniteNumber.validate(0) // Throws ValidationError: "Expected a non-zero number, got 0"
|
|
1244
|
-
* T.nonZeroFiniteNumber.validate(Infinity) // Throws ValidationError
|
|
1245
|
-
* ```
|
|
1246
|
-
* @public
|
|
1247
|
-
*/
|
|
1248
|
-
export const nonZeroFiniteNumber = new Validator<number>((value) => {
|
|
1249
|
-
if (Number.isFinite(value) && (value as number) !== 0) {
|
|
1250
|
-
return value as number
|
|
1251
|
-
}
|
|
1252
|
-
if (typeof value !== 'number') {
|
|
1253
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1254
|
-
}
|
|
1255
|
-
if (value !== value) {
|
|
1256
|
-
throw new ValidationError('Expected a number, got NaN')
|
|
1257
|
-
}
|
|
1258
|
-
if (value === 0) {
|
|
1259
|
-
throw new ValidationError(`Expected a non-zero number, got 0`)
|
|
1260
|
-
}
|
|
1261
|
-
throw new ValidationError(`Expected a finite number, got ${value}`)
|
|
1262
|
-
})
|
|
1263
|
-
/**
|
|
1264
|
-
* Validator that ensures a value is a number in the unit interval [0, 1].
|
|
1265
|
-
* Useful for opacity, percentages expressed as decimals, and other normalized values.
|
|
1266
|
-
*
|
|
1267
|
-
* @example
|
|
1268
|
-
* ```ts
|
|
1269
|
-
* const opacity = T.unitInterval.validate(0.5) // Returns 0.5
|
|
1270
|
-
* T.unitInterval.validate(0) // Returns 0 (valid)
|
|
1271
|
-
* T.unitInterval.validate(1) // Returns 1 (valid)
|
|
1272
|
-
* T.unitInterval.validate(1.5) // Throws ValidationError
|
|
1273
|
-
* T.unitInterval.validate(-0.1) // Throws ValidationError
|
|
1274
|
-
* ```
|
|
1275
|
-
* @public
|
|
1276
|
-
*/
|
|
1277
|
-
export const unitInterval = new Validator<number>((value) => {
|
|
1278
|
-
if (Number.isFinite(value) && (value as number) >= 0 && (value as number) <= 1) {
|
|
1279
|
-
return value as number
|
|
1280
|
-
}
|
|
1281
|
-
if (typeof value !== 'number') {
|
|
1282
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1283
|
-
}
|
|
1284
|
-
if (value !== value) {
|
|
1285
|
-
throw new ValidationError('Expected a number, got NaN')
|
|
1286
|
-
}
|
|
1287
|
-
throw new ValidationError(`Expected a number between 0 and 1, got ${value}`)
|
|
1045
|
+
export const nonZeroNumber = number.check((value) => {
|
|
1046
|
+
if (value <= 0) throw new ValidationError(`Expected a non-zero positive number, got ${value}`)
|
|
1288
1047
|
})
|
|
1289
1048
|
/**
|
|
1290
1049
|
* Validator that ensures a value is an integer (whole number).
|
|
@@ -1297,21 +1056,8 @@ export const unitInterval = new Validator<number>((value) => {
|
|
|
1297
1056
|
* ```
|
|
1298
1057
|
* @public
|
|
1299
1058
|
*/
|
|
1300
|
-
export const integer =
|
|
1301
|
-
|
|
1302
|
-
if (Number.isInteger(value)) {
|
|
1303
|
-
return value as number
|
|
1304
|
-
}
|
|
1305
|
-
if (typeof value !== 'number') {
|
|
1306
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1307
|
-
}
|
|
1308
|
-
if (value !== value) {
|
|
1309
|
-
throw new ValidationError('Expected a number, got NaN')
|
|
1310
|
-
}
|
|
1311
|
-
if (value - value !== 0) {
|
|
1312
|
-
throw new ValidationError(`Expected a finite number, got ${value}`)
|
|
1313
|
-
}
|
|
1314
|
-
throw new ValidationError(`Expected an integer, got ${value}`)
|
|
1059
|
+
export const integer = number.check((value) => {
|
|
1060
|
+
if (!Number.isInteger(value)) throw new ValidationError(`Expected an integer, got ${value}`)
|
|
1315
1061
|
})
|
|
1316
1062
|
/**
|
|
1317
1063
|
* Validator that ensures a value is a non-negative integer (\>= 0).
|
|
@@ -1326,23 +1072,8 @@ export const integer = new Validator<number>((value) => {
|
|
|
1326
1072
|
* ```
|
|
1327
1073
|
* @public
|
|
1328
1074
|
*/
|
|
1329
|
-
export const positiveInteger =
|
|
1330
|
-
if (
|
|
1331
|
-
return value as number
|
|
1332
|
-
}
|
|
1333
|
-
if (typeof value !== 'number') {
|
|
1334
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1335
|
-
}
|
|
1336
|
-
if (value !== value) {
|
|
1337
|
-
throw new ValidationError('Expected a number, got NaN')
|
|
1338
|
-
}
|
|
1339
|
-
if (value - value !== 0) {
|
|
1340
|
-
throw new ValidationError(`Expected a finite number, got ${value}`)
|
|
1341
|
-
}
|
|
1342
|
-
if (value < 0) {
|
|
1343
|
-
throw new ValidationError(`Expected a positive integer, got ${value}`)
|
|
1344
|
-
}
|
|
1345
|
-
throw new ValidationError(`Expected an integer, got ${value}`)
|
|
1075
|
+
export const positiveInteger = integer.check((value) => {
|
|
1076
|
+
if (value < 0) throw new ValidationError(`Expected a positive integer, got ${value}`)
|
|
1346
1077
|
})
|
|
1347
1078
|
/**
|
|
1348
1079
|
* Validator that ensures a value is a positive integer (\> 0). Rejects zero and negative integers.
|
|
@@ -1355,23 +1086,8 @@ export const positiveInteger = new Validator<number>((value) => {
|
|
|
1355
1086
|
* ```
|
|
1356
1087
|
* @public
|
|
1357
1088
|
*/
|
|
1358
|
-
export const nonZeroInteger =
|
|
1359
|
-
if (
|
|
1360
|
-
return value as number
|
|
1361
|
-
}
|
|
1362
|
-
if (typeof value !== 'number') {
|
|
1363
|
-
throw new ValidationError(`Expected number, got ${typeToString(value)}`)
|
|
1364
|
-
}
|
|
1365
|
-
if (value !== value) {
|
|
1366
|
-
throw new ValidationError('Expected a number, got NaN')
|
|
1367
|
-
}
|
|
1368
|
-
if (value - value !== 0) {
|
|
1369
|
-
throw new ValidationError(`Expected a finite number, got ${value}`)
|
|
1370
|
-
}
|
|
1371
|
-
if (value <= 0) {
|
|
1372
|
-
throw new ValidationError(`Expected a non-zero positive integer, got ${value}`)
|
|
1373
|
-
}
|
|
1374
|
-
throw new ValidationError(`Expected an integer, got ${value}`)
|
|
1089
|
+
export const nonZeroInteger = integer.check((value) => {
|
|
1090
|
+
if (value <= 0) throw new ValidationError(`Expected a non-zero positive integer, got ${value}`)
|
|
1375
1091
|
})
|
|
1376
1092
|
|
|
1377
1093
|
/**
|
|
@@ -1820,14 +1536,13 @@ export function optional<T>(validator: Validatable<T>): Validator<T | undefined>
|
|
|
1820
1536
|
return validator.validate(value)
|
|
1821
1537
|
},
|
|
1822
1538
|
(knownGoodValue, newValue) => {
|
|
1539
|
+
if (knownGoodValue === undefined && newValue === undefined) return undefined
|
|
1823
1540
|
if (newValue === undefined) return undefined
|
|
1824
1541
|
if (validator.validateUsingKnownGoodVersion && knownGoodValue !== undefined) {
|
|
1825
1542
|
return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
|
|
1826
1543
|
}
|
|
1827
1544
|
return validator.validate(newValue)
|
|
1828
|
-
}
|
|
1829
|
-
// Propagate skipSameValueCheck from inner validator to allow refine wrappers
|
|
1830
|
-
validator instanceof Validator && validator.skipSameValueCheck
|
|
1545
|
+
}
|
|
1831
1546
|
)
|
|
1832
1547
|
}
|
|
1833
1548
|
|
|
@@ -1857,9 +1572,7 @@ export function nullable<T>(validator: Validatable<T>): Validator<T | null> {
|
|
|
1857
1572
|
return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
|
|
1858
1573
|
}
|
|
1859
1574
|
return validator.validate(newValue)
|
|
1860
|
-
}
|
|
1861
|
-
// Propagate skipSameValueCheck from inner validator to allow refine wrappers
|
|
1862
|
-
validator instanceof Validator && validator.skipSameValueCheck
|
|
1575
|
+
}
|
|
1863
1576
|
)
|
|
1864
1577
|
}
|
|
1865
1578
|
|
|
@@ -117,56 +117,23 @@ describe('validations', () => {
|
|
|
117
117
|
`[ValidationError: At animal(type = cat).meow: Expected boolean, got undefined]`
|
|
118
118
|
)
|
|
119
119
|
})
|
|
120
|
-
|
|
121
|
-
it('Rejects Infinity and -Infinity in numberUnion discriminators', () => {
|
|
122
|
-
const numberUnionSchema = T.numberUnion('version', {
|
|
123
|
-
1: T.object({ version: T.literal(1), data: T.string }),
|
|
124
|
-
2: T.object({ version: T.literal(2), data: T.string }),
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
// Valid cases
|
|
128
|
-
expect(numberUnionSchema.validate({ version: 1, data: 'hello' })).toEqual({
|
|
129
|
-
version: 1,
|
|
130
|
-
data: 'hello',
|
|
131
|
-
})
|
|
132
|
-
expect(numberUnionSchema.validate({ version: 2, data: 'world' })).toEqual({
|
|
133
|
-
version: 2,
|
|
134
|
-
data: 'world',
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// Should reject Infinity
|
|
138
|
-
expect(() =>
|
|
139
|
-
numberUnionSchema.validate({ version: Infinity, data: 'test' })
|
|
140
|
-
).toThrowErrorMatchingInlineSnapshot(
|
|
141
|
-
`[ValidationError: At null: Expected a number for key "version", got "Infinity"]`
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
// Should reject -Infinity
|
|
145
|
-
expect(() =>
|
|
146
|
-
numberUnionSchema.validate({ version: -Infinity, data: 'test' })
|
|
147
|
-
).toThrowErrorMatchingInlineSnapshot(
|
|
148
|
-
`[ValidationError: At null: Expected a number for key "version", got "-Infinity"]`
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
// Should reject NaN
|
|
152
|
-
expect(() => numberUnionSchema.validate({ version: NaN, data: 'test' })).toThrowError(
|
|
153
|
-
/Expected a number for key "version"/
|
|
154
|
-
)
|
|
155
|
-
})
|
|
156
120
|
})
|
|
157
121
|
|
|
158
122
|
describe('T.refine', () => {
|
|
159
123
|
it('Refines a validator.', () => {
|
|
160
|
-
// refine can transform values (e.g., string to number)
|
|
161
124
|
const stringToNumber = T.string.refine((str) => parseInt(str, 10))
|
|
125
|
+
const originalEnv = process.env.NODE_ENV
|
|
126
|
+
process.env.NODE_ENV = 'production'
|
|
162
127
|
expect(stringToNumber.validate('42')).toBe(42)
|
|
128
|
+
process.env.NODE_ENV = originalEnv
|
|
163
129
|
|
|
164
|
-
// refine can also modify values of the same type
|
|
165
130
|
const prefixedString = T.string.refine((str) =>
|
|
166
131
|
str.startsWith('prefix:') ? str : `prefix:${str}`
|
|
167
132
|
)
|
|
133
|
+
process.env.NODE_ENV = 'production'
|
|
168
134
|
expect(prefixedString.validate('test')).toBe('prefix:test')
|
|
169
135
|
expect(prefixedString.validate('prefix:existing')).toBe('prefix:existing')
|
|
136
|
+
process.env.NODE_ENV = originalEnv
|
|
170
137
|
})
|
|
171
138
|
|
|
172
139
|
it('Produces a type error if the refinement is not of the correct type.', () => {
|