@chidchanun/bcp 0.1.18 → 0.1.19

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.
@@ -0,0 +1,1118 @@
1
+ export type ValidationPathSegment =
2
+ | string
3
+ | number;
4
+
5
+ export type ValidationPath =
6
+ readonly ValidationPathSegment[];
7
+
8
+ export interface ValidationIssue {
9
+ path: ValidationPath;
10
+ message: string;
11
+ code: string;
12
+ }
13
+
14
+ export interface ValidationSuccess<T> {
15
+ success: true;
16
+ data: T;
17
+ }
18
+
19
+ export interface ValidationFailure {
20
+ success: false;
21
+ issues: ValidationIssue[];
22
+ fieldErrors: Record<string, string[]>;
23
+ formErrors: string[];
24
+ }
25
+
26
+ export type ValidationResult<T> =
27
+ | ValidationSuccess<T>
28
+ | ValidationFailure;
29
+
30
+ export interface Validator<T> {
31
+ safeParse(
32
+ input: unknown,
33
+ path?: ValidationPath
34
+ ): ValidationResult<T>;
35
+ parse(input: unknown): T;
36
+ optional(): Validator<T | undefined>;
37
+ nullable(): Validator<T | null>;
38
+ refine(
39
+ predicate: (value: T) => boolean,
40
+ message: string,
41
+ code?: string
42
+ ): Validator<T>;
43
+ }
44
+
45
+ export type InferValidator<TValidator> =
46
+ TValidator extends Validator<infer TValue>
47
+ ? TValue
48
+ : never;
49
+
50
+ export type ValidationShape =
51
+ Record<string, Validator<any>>;
52
+
53
+ export type InferValidationShape<
54
+ TShape extends ValidationShape
55
+ > = {
56
+ [TKey in keyof TShape]:
57
+ InferValidator<TShape[TKey]>;
58
+ };
59
+
60
+ export interface StringValidationOptions {
61
+ trim?: boolean;
62
+ minLength?: number;
63
+ maxLength?: number;
64
+ email?: boolean;
65
+ pattern?: RegExp;
66
+ }
67
+
68
+ export interface NumberValidationOptions {
69
+ coerce?: boolean;
70
+ integer?: boolean;
71
+ min?: number;
72
+ max?: number;
73
+ }
74
+
75
+ export interface BooleanValidationOptions {
76
+ coerce?: boolean;
77
+ }
78
+
79
+ export interface ArrayValidationOptions {
80
+ minLength?: number;
81
+ maxLength?: number;
82
+ }
83
+
84
+ export interface ObjectValidationOptions {
85
+ allowUnknown?: boolean;
86
+ }
87
+
88
+ export class ValidationError
89
+ extends Error {
90
+ readonly issues:
91
+ ValidationIssue[];
92
+ readonly fieldErrors:
93
+ Record<string, string[]>;
94
+ readonly formErrors:
95
+ string[];
96
+
97
+ constructor(
98
+ issues: readonly ValidationIssue[],
99
+ message = "Validation failed."
100
+ ) {
101
+ super(message);
102
+ this.name =
103
+ "ValidationError";
104
+
105
+ const failure =
106
+ createFailure(
107
+ issues
108
+ );
109
+
110
+ this.issues =
111
+ failure.issues;
112
+ this.fieldErrors =
113
+ failure.fieldErrors;
114
+ this.formErrors =
115
+ failure.formErrors;
116
+ }
117
+ }
118
+
119
+ class BcpValidator<T>
120
+ implements Validator<T> {
121
+ constructor(
122
+ private readonly parser:
123
+ (
124
+ input: unknown,
125
+ path: ValidationPath
126
+ ) => ValidationResult<T>
127
+ ) {}
128
+
129
+ safeParse(
130
+ input: unknown,
131
+ path: ValidationPath = []
132
+ ): ValidationResult<T> {
133
+ return this.parser(
134
+ input,
135
+ [
136
+ ...path,
137
+ ]
138
+ );
139
+ }
140
+
141
+ parse(input: unknown): T {
142
+ const result =
143
+ this.safeParse(
144
+ input
145
+ );
146
+
147
+ if (!result.success) {
148
+ throw new ValidationError(
149
+ result.issues
150
+ );
151
+ }
152
+
153
+ return result.data;
154
+ }
155
+
156
+ optional():
157
+ Validator<T | undefined> {
158
+ return new BcpValidator<
159
+ T | undefined
160
+ >(
161
+ (
162
+ input,
163
+ path
164
+ ) => {
165
+ if (
166
+ input ===
167
+ undefined
168
+ ) {
169
+ return success(
170
+ undefined
171
+ );
172
+ }
173
+
174
+ return this.safeParse(
175
+ input,
176
+ path
177
+ );
178
+ }
179
+ );
180
+ }
181
+
182
+ nullable():
183
+ Validator<T | null> {
184
+ return new BcpValidator<
185
+ T | null
186
+ >(
187
+ (
188
+ input,
189
+ path
190
+ ) => {
191
+ if (
192
+ input === null
193
+ ) {
194
+ return success(
195
+ null
196
+ );
197
+ }
198
+
199
+ return this.safeParse(
200
+ input,
201
+ path
202
+ );
203
+ }
204
+ );
205
+ }
206
+
207
+ refine(
208
+ predicate:
209
+ (value: T) => boolean,
210
+ message: string,
211
+ code = "custom"
212
+ ): Validator<T> {
213
+ return new BcpValidator<T>(
214
+ (
215
+ input,
216
+ path
217
+ ) => {
218
+ const result =
219
+ this.safeParse(
220
+ input,
221
+ path
222
+ );
223
+
224
+ if (!result.success) {
225
+ return result;
226
+ }
227
+
228
+ let accepted =
229
+ false;
230
+
231
+ try {
232
+ accepted =
233
+ predicate(
234
+ result.data
235
+ );
236
+ } catch {
237
+ accepted =
238
+ false;
239
+ }
240
+
241
+ if (!accepted) {
242
+ return failure(
243
+ path,
244
+ message,
245
+ code
246
+ );
247
+ }
248
+
249
+ return result;
250
+ }
251
+ );
252
+ }
253
+ }
254
+
255
+ export function string(
256
+ options:
257
+ StringValidationOptions = {}
258
+ ): Validator<string> {
259
+ validateLengthOption(
260
+ options.minLength,
261
+ "minLength"
262
+ );
263
+ validateLengthOption(
264
+ options.maxLength,
265
+ "maxLength"
266
+ );
267
+
268
+ if (
269
+ options.minLength !== undefined &&
270
+ options.maxLength !== undefined &&
271
+ options.minLength >
272
+ options.maxLength
273
+ ) {
274
+ throw new Error(
275
+ "BCP Validation: string minLength cannot be greater than maxLength."
276
+ );
277
+ }
278
+
279
+ return new BcpValidator<string>(
280
+ (
281
+ input,
282
+ path
283
+ ) => {
284
+ if (
285
+ typeof input !==
286
+ "string"
287
+ ) {
288
+ return requiredOrTypeFailure(
289
+ input,
290
+ path,
291
+ "string"
292
+ );
293
+ }
294
+
295
+ const value =
296
+ options.trim
297
+ ? input.trim()
298
+ : input;
299
+
300
+ if (
301
+ options.minLength !== undefined &&
302
+ value.length <
303
+ options.minLength
304
+ ) {
305
+ return failure(
306
+ path,
307
+ `Must contain at least ${options.minLength} character(s).`,
308
+ "too_small"
309
+ );
310
+ }
311
+
312
+ if (
313
+ options.maxLength !== undefined &&
314
+ value.length >
315
+ options.maxLength
316
+ ) {
317
+ return failure(
318
+ path,
319
+ `Must contain at most ${options.maxLength} character(s).`,
320
+ "too_big"
321
+ );
322
+ }
323
+
324
+ if (
325
+ options.email &&
326
+ !isEmail(
327
+ value
328
+ )
329
+ ) {
330
+ return failure(
331
+ path,
332
+ "Must be a valid email address.",
333
+ "invalid_email"
334
+ );
335
+ }
336
+
337
+ if (
338
+ options.pattern &&
339
+ !testPattern(
340
+ options.pattern,
341
+ value
342
+ )
343
+ ) {
344
+ return failure(
345
+ path,
346
+ "Must match the required format.",
347
+ "invalid_string"
348
+ );
349
+ }
350
+
351
+ return success(
352
+ value
353
+ );
354
+ }
355
+ );
356
+ }
357
+
358
+ export function number(
359
+ options:
360
+ NumberValidationOptions = {}
361
+ ): Validator<number> {
362
+ return new BcpValidator<number>(
363
+ (
364
+ input,
365
+ path
366
+ ) => {
367
+ let value =
368
+ input;
369
+
370
+ if (
371
+ options.coerce &&
372
+ typeof value ===
373
+ "string" &&
374
+ value.trim() !==
375
+ ""
376
+ ) {
377
+ value =
378
+ Number(
379
+ value
380
+ );
381
+ }
382
+
383
+ if (
384
+ typeof value !==
385
+ "number" ||
386
+ !Number.isFinite(
387
+ value
388
+ )
389
+ ) {
390
+ return requiredOrTypeFailure(
391
+ input,
392
+ path,
393
+ "number"
394
+ );
395
+ }
396
+
397
+ if (
398
+ options.integer &&
399
+ !Number.isInteger(
400
+ value
401
+ )
402
+ ) {
403
+ return failure(
404
+ path,
405
+ "Must be an integer.",
406
+ "invalid_integer"
407
+ );
408
+ }
409
+
410
+ if (
411
+ options.min !== undefined &&
412
+ value < options.min
413
+ ) {
414
+ return failure(
415
+ path,
416
+ `Must be greater than or equal to ${options.min}.`,
417
+ "too_small"
418
+ );
419
+ }
420
+
421
+ if (
422
+ options.max !== undefined &&
423
+ value > options.max
424
+ ) {
425
+ return failure(
426
+ path,
427
+ `Must be less than or equal to ${options.max}.`,
428
+ "too_big"
429
+ );
430
+ }
431
+
432
+ return success(
433
+ value
434
+ );
435
+ }
436
+ );
437
+ }
438
+
439
+ export function boolean(
440
+ options:
441
+ BooleanValidationOptions = {}
442
+ ): Validator<boolean> {
443
+ return new BcpValidator<boolean>(
444
+ (
445
+ input,
446
+ path
447
+ ) => {
448
+ if (
449
+ typeof input ===
450
+ "boolean"
451
+ ) {
452
+ return success(
453
+ input
454
+ );
455
+ }
456
+
457
+ if (
458
+ options.coerce &&
459
+ typeof input ===
460
+ "string"
461
+ ) {
462
+ const normalized =
463
+ input
464
+ .trim()
465
+ .toLowerCase();
466
+
467
+ if (
468
+ [
469
+ "1",
470
+ "true",
471
+ "on",
472
+ "yes",
473
+ ].includes(
474
+ normalized
475
+ )
476
+ ) {
477
+ return success(
478
+ true
479
+ );
480
+ }
481
+
482
+ if (
483
+ [
484
+ "0",
485
+ "false",
486
+ "off",
487
+ "no",
488
+ ].includes(
489
+ normalized
490
+ )
491
+ ) {
492
+ return success(
493
+ false
494
+ );
495
+ }
496
+ }
497
+
498
+ return requiredOrTypeFailure(
499
+ input,
500
+ path,
501
+ "boolean"
502
+ );
503
+ }
504
+ );
505
+ }
506
+
507
+ export function literal<
508
+ const TValue extends
509
+ | string
510
+ | number
511
+ | boolean
512
+ | null
513
+ >(
514
+ expected: TValue
515
+ ): Validator<TValue> {
516
+ return new BcpValidator<TValue>(
517
+ (
518
+ input,
519
+ path
520
+ ) => {
521
+ if (
522
+ input !==
523
+ expected
524
+ ) {
525
+ return failure(
526
+ path,
527
+ `Must equal ${JSON.stringify(expected)}.`,
528
+ "invalid_literal"
529
+ );
530
+ }
531
+
532
+ return success(
533
+ expected
534
+ );
535
+ }
536
+ );
537
+ }
538
+
539
+ export function enumValue<
540
+ const TValues extends
541
+ readonly [
542
+ string,
543
+ ...string[],
544
+ ]
545
+ >(
546
+ values: TValues
547
+ ): Validator<TValues[number]> {
548
+ const accepted =
549
+ new Set<string>(
550
+ values
551
+ );
552
+
553
+ return new BcpValidator<
554
+ TValues[number]
555
+ >(
556
+ (
557
+ input,
558
+ path
559
+ ) => {
560
+ if (
561
+ typeof input !==
562
+ "string" ||
563
+ !accepted.has(
564
+ input
565
+ )
566
+ ) {
567
+ return failure(
568
+ path,
569
+ `Must be one of: ${values.join(", ")}.`,
570
+ "invalid_enum"
571
+ );
572
+ }
573
+
574
+ return success(
575
+ input as TValues[number]
576
+ );
577
+ }
578
+ );
579
+ }
580
+
581
+ export function array<T>(
582
+ item: Validator<T>,
583
+ options:
584
+ ArrayValidationOptions = {}
585
+ ): Validator<T[]> {
586
+ validateLengthOption(
587
+ options.minLength,
588
+ "minLength"
589
+ );
590
+ validateLengthOption(
591
+ options.maxLength,
592
+ "maxLength"
593
+ );
594
+
595
+ return new BcpValidator<T[]>(
596
+ (
597
+ input,
598
+ path
599
+ ) => {
600
+ if (
601
+ !Array.isArray(
602
+ input
603
+ )
604
+ ) {
605
+ return requiredOrTypeFailure(
606
+ input,
607
+ path,
608
+ "array"
609
+ );
610
+ }
611
+
612
+ if (
613
+ options.minLength !== undefined &&
614
+ input.length <
615
+ options.minLength
616
+ ) {
617
+ return failure(
618
+ path,
619
+ `Must contain at least ${options.minLength} item(s).`,
620
+ "too_small"
621
+ );
622
+ }
623
+
624
+ if (
625
+ options.maxLength !== undefined &&
626
+ input.length >
627
+ options.maxLength
628
+ ) {
629
+ return failure(
630
+ path,
631
+ `Must contain at most ${options.maxLength} item(s).`,
632
+ "too_big"
633
+ );
634
+ }
635
+
636
+ const output:
637
+ T[] = [];
638
+ const issues:
639
+ ValidationIssue[] = [];
640
+
641
+ for (
642
+ let index = 0;
643
+ index < input.length;
644
+ index += 1
645
+ ) {
646
+ const result =
647
+ item.safeParse(
648
+ input[index],
649
+ [
650
+ ...path,
651
+ index,
652
+ ]
653
+ );
654
+
655
+ if (result.success) {
656
+ output.push(
657
+ result.data
658
+ );
659
+ } else {
660
+ issues.push(
661
+ ...result.issues
662
+ );
663
+ }
664
+ }
665
+
666
+ return issues.length > 0
667
+ ? createFailure(
668
+ issues
669
+ )
670
+ : success(
671
+ output
672
+ );
673
+ }
674
+ );
675
+ }
676
+
677
+ export function object<
678
+ TShape extends ValidationShape
679
+ >(
680
+ shape: TShape,
681
+ options:
682
+ ObjectValidationOptions = {}
683
+ ): Validator<
684
+ InferValidationShape<TShape>
685
+ > {
686
+ return new BcpValidator<
687
+ InferValidationShape<TShape>
688
+ >(
689
+ (
690
+ input,
691
+ path
692
+ ) => {
693
+ if (
694
+ !isPlainObject(
695
+ input
696
+ )
697
+ ) {
698
+ return requiredOrTypeFailure(
699
+ input,
700
+ path,
701
+ "object"
702
+ );
703
+ }
704
+
705
+ const source =
706
+ input as Record<
707
+ string,
708
+ unknown
709
+ >;
710
+ const output:
711
+ Record<string, unknown> =
712
+ options.allowUnknown
713
+ ? {
714
+ ...source,
715
+ }
716
+ : {};
717
+ const issues:
718
+ ValidationIssue[] = [];
719
+
720
+ for (
721
+ const [
722
+ key,
723
+ validator,
724
+ ]
725
+ of Object.entries(
726
+ shape
727
+ )
728
+ ) {
729
+ const result =
730
+ validator.safeParse(
731
+ source[key],
732
+ [
733
+ ...path,
734
+ key,
735
+ ]
736
+ );
737
+
738
+ if (result.success) {
739
+ output[key] =
740
+ result.data;
741
+ } else {
742
+ issues.push(
743
+ ...result.issues
744
+ );
745
+ }
746
+ }
747
+
748
+ return issues.length > 0
749
+ ? createFailure(
750
+ issues
751
+ )
752
+ : success(
753
+ output as
754
+ InferValidationShape<TShape>
755
+ );
756
+ }
757
+ );
758
+ }
759
+
760
+ export function union<
761
+ const TValidators extends
762
+ readonly [
763
+ Validator<any>,
764
+ ...Validator<any>[],
765
+ ]
766
+ >(
767
+ validators: TValidators
768
+ ): Validator<
769
+ InferValidator<
770
+ TValidators[number]
771
+ >
772
+ > {
773
+ return new BcpValidator<
774
+ InferValidator<
775
+ TValidators[number]
776
+ >
777
+ >(
778
+ (
779
+ input,
780
+ path
781
+ ) => {
782
+ const issues:
783
+ ValidationIssue[] = [];
784
+
785
+ for (
786
+ const validator
787
+ of validators
788
+ ) {
789
+ const result =
790
+ validator.safeParse(
791
+ input,
792
+ path
793
+ );
794
+
795
+ if (result.success) {
796
+ return result;
797
+ }
798
+
799
+ issues.push(
800
+ ...result.issues
801
+ );
802
+ }
803
+
804
+ return createFailure(
805
+ issues.length > 0
806
+ ? issues
807
+ : [
808
+ {
809
+ path,
810
+ message:
811
+ "Value did not match any allowed type.",
812
+ code:
813
+ "invalid_union",
814
+ },
815
+ ]
816
+ );
817
+ }
818
+ );
819
+ }
820
+
821
+ export function optional<T>(
822
+ validator: Validator<T>
823
+ ): Validator<T | undefined> {
824
+ return validator.optional();
825
+ }
826
+
827
+ export function nullable<T>(
828
+ validator: Validator<T>
829
+ ): Validator<T | null> {
830
+ return validator.nullable();
831
+ }
832
+
833
+ export function safeParse<T>(
834
+ validator: Validator<T>,
835
+ input: unknown
836
+ ): ValidationResult<T> {
837
+ return validator.safeParse(
838
+ input
839
+ );
840
+ }
841
+
842
+ export function parse<T>(
843
+ validator: Validator<T>,
844
+ input: unknown
845
+ ): T {
846
+ return validator.parse(
847
+ input
848
+ );
849
+ }
850
+
851
+ export function validateFormData<T>(
852
+ validator: Validator<T>,
853
+ formData: FormData
854
+ ): ValidationResult<T> {
855
+ return validator.safeParse(
856
+ formDataToObject(
857
+ formData
858
+ )
859
+ );
860
+ }
861
+
862
+ export function formDataToObject(
863
+ formData: FormData
864
+ ): Record<string, unknown> {
865
+ const result:
866
+ Record<string, unknown> = {};
867
+
868
+ for (
869
+ const [
870
+ key,
871
+ value,
872
+ ]
873
+ of formData.entries()
874
+ ) {
875
+ const existing =
876
+ result[key];
877
+
878
+ if (
879
+ existing === undefined
880
+ ) {
881
+ result[key] =
882
+ value;
883
+ continue;
884
+ }
885
+
886
+ if (
887
+ Array.isArray(
888
+ existing
889
+ )
890
+ ) {
891
+ existing.push(
892
+ value
893
+ );
894
+ continue;
895
+ }
896
+
897
+ result[key] = [
898
+ existing,
899
+ value,
900
+ ];
901
+ }
902
+
903
+ return result;
904
+ }
905
+
906
+ export function flattenValidationIssues(
907
+ issues: readonly ValidationIssue[]
908
+ ): Pick<
909
+ ValidationFailure,
910
+ "fieldErrors" | "formErrors"
911
+ > {
912
+ const fieldErrors:
913
+ Record<string, string[]> = {};
914
+ const formErrors:
915
+ string[] = [];
916
+
917
+ for (
918
+ const issue
919
+ of issues
920
+ ) {
921
+ if (
922
+ issue.path.length ===
923
+ 0
924
+ ) {
925
+ formErrors.push(
926
+ issue.message
927
+ );
928
+ continue;
929
+ }
930
+
931
+ const field =
932
+ formatValidationPath(
933
+ issue.path
934
+ );
935
+
936
+ fieldErrors[field] ??=
937
+ [];
938
+ fieldErrors[field].push(
939
+ issue.message
940
+ );
941
+ }
942
+
943
+ return {
944
+ fieldErrors,
945
+ formErrors,
946
+ };
947
+ }
948
+
949
+ export function getFieldError(
950
+ result:
951
+ ValidationResult<unknown>,
952
+ field: string
953
+ ): string | undefined {
954
+ if (result.success) {
955
+ return undefined;
956
+ }
957
+
958
+ return result
959
+ .fieldErrors[field]?.[0];
960
+ }
961
+
962
+ export function isValidationError(
963
+ value: unknown
964
+ ): value is ValidationError {
965
+ return value instanceof
966
+ ValidationError;
967
+ }
968
+
969
+ export function formatValidationPath(
970
+ path: ValidationPath
971
+ ): string {
972
+ return path
973
+ .map(
974
+ String
975
+ )
976
+ .join(".");
977
+ }
978
+
979
+ export const v = {
980
+ string,
981
+ number,
982
+ boolean,
983
+ literal,
984
+ enum:
985
+ enumValue,
986
+ array,
987
+ object,
988
+ union,
989
+ optional,
990
+ nullable,
991
+ } as const;
992
+
993
+ function success<T>(
994
+ data: T
995
+ ): ValidationSuccess<T> {
996
+ return {
997
+ success: true,
998
+ data,
999
+ };
1000
+ }
1001
+
1002
+ function failure(
1003
+ path: ValidationPath,
1004
+ message: string,
1005
+ code: string
1006
+ ): ValidationFailure {
1007
+ return createFailure([
1008
+ {
1009
+ path:
1010
+ [
1011
+ ...path,
1012
+ ],
1013
+ message,
1014
+ code,
1015
+ },
1016
+ ]);
1017
+ }
1018
+
1019
+ function createFailure(
1020
+ sourceIssues:
1021
+ readonly ValidationIssue[]
1022
+ ): ValidationFailure {
1023
+ const issues =
1024
+ sourceIssues.map(
1025
+ (issue) => ({
1026
+ ...issue,
1027
+ path: [
1028
+ ...issue.path,
1029
+ ],
1030
+ })
1031
+ );
1032
+ const flattened =
1033
+ flattenValidationIssues(
1034
+ issues
1035
+ );
1036
+
1037
+ return {
1038
+ success: false,
1039
+ issues,
1040
+ ...flattened,
1041
+ };
1042
+ }
1043
+
1044
+ function requiredOrTypeFailure(
1045
+ input: unknown,
1046
+ path: ValidationPath,
1047
+ expected: string
1048
+ ): ValidationFailure {
1049
+ if (
1050
+ input === undefined ||
1051
+ input === null ||
1052
+ input === ""
1053
+ ) {
1054
+ return failure(
1055
+ path,
1056
+ "Required.",
1057
+ "required"
1058
+ );
1059
+ }
1060
+
1061
+ return failure(
1062
+ path,
1063
+ `Expected ${expected}.`,
1064
+ "invalid_type"
1065
+ );
1066
+ }
1067
+
1068
+ function validateLengthOption(
1069
+ value: number | undefined,
1070
+ name: string
1071
+ ): void {
1072
+ if (
1073
+ value !== undefined &&
1074
+ (
1075
+ !Number.isInteger(
1076
+ value
1077
+ ) ||
1078
+ value < 0
1079
+ )
1080
+ ) {
1081
+ throw new Error(
1082
+ `BCP Validation: ${name} must be a non-negative integer.`
1083
+ );
1084
+ }
1085
+ }
1086
+
1087
+ function isPlainObject(
1088
+ value: unknown
1089
+ ): value is Record<string, unknown> {
1090
+ return (
1091
+ typeof value ===
1092
+ "object" &&
1093
+ value !== null &&
1094
+ !Array.isArray(
1095
+ value
1096
+ )
1097
+ );
1098
+ }
1099
+
1100
+ function isEmail(
1101
+ value: string
1102
+ ): boolean {
1103
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(
1104
+ value
1105
+ );
1106
+ }
1107
+
1108
+ function testPattern(
1109
+ pattern: RegExp,
1110
+ value: string
1111
+ ): boolean {
1112
+ pattern.lastIndex =
1113
+ 0;
1114
+
1115
+ return pattern.test(
1116
+ value
1117
+ );
1118
+ }