@hy_ong/zod-kit 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -97
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +3127 -146
  7. package/dist/index.d.cts +3021 -25
  8. package/dist/index.d.ts +3021 -25
  9. package/dist/index.js +3081 -144
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +161 -25
  14. package/src/i18n/locales/zh-TW.json +165 -26
  15. package/src/index.ts +17 -7
  16. package/src/validators/common/boolean.ts +191 -0
  17. package/src/validators/common/date.ts +299 -0
  18. package/src/validators/common/datetime.ts +673 -0
  19. package/src/validators/common/email.ts +313 -0
  20. package/src/validators/common/file.ts +384 -0
  21. package/src/validators/common/id.ts +471 -0
  22. package/src/validators/common/number.ts +319 -0
  23. package/src/validators/common/password.ts +386 -0
  24. package/src/validators/common/text.ts +271 -0
  25. package/src/validators/common/time.ts +600 -0
  26. package/src/validators/common/url.ts +347 -0
  27. package/src/validators/taiwan/business-id.ts +262 -0
  28. package/src/validators/taiwan/fax.ts +327 -0
  29. package/src/validators/taiwan/mobile.ts +242 -0
  30. package/src/validators/taiwan/national-id.ts +425 -0
  31. package/src/validators/taiwan/postal-code.ts +1049 -0
  32. package/src/validators/taiwan/tel.ts +330 -0
  33. package/tests/common/boolean.test.ts +340 -92
  34. package/tests/common/date.test.ts +458 -0
  35. package/tests/common/datetime.test.ts +693 -0
  36. package/tests/common/email.test.ts +232 -60
  37. package/tests/common/file.test.ts +479 -0
  38. package/tests/common/id.test.ts +535 -0
  39. package/tests/common/number.test.ts +230 -60
  40. package/tests/common/password.test.ts +271 -44
  41. package/tests/common/text.test.ts +210 -13
  42. package/tests/common/time.test.ts +528 -0
  43. package/tests/common/url.test.ts +492 -67
  44. package/tests/taiwan/business-id.test.ts +240 -0
  45. package/tests/taiwan/fax.test.ts +463 -0
  46. package/tests/taiwan/mobile.test.ts +373 -0
  47. package/tests/taiwan/national-id.test.ts +435 -0
  48. package/tests/taiwan/postal-code.test.ts +705 -0
  49. package/tests/taiwan/tel.test.ts +467 -0
  50. package/eslint.config.mjs +0 -10
  51. package/src/common/boolean.ts +0 -36
  52. package/src/common/date.ts +0 -43
  53. package/src/common/email.ts +0 -44
  54. package/src/common/integer.ts +0 -46
  55. package/src/common/number.ts +0 -37
  56. package/src/common/password.ts +0 -33
  57. package/src/common/text.ts +0 -34
  58. package/src/common/url.ts +0 -37
  59. package/tests/common/integer.test.ts +0 -90
@@ -0,0 +1,467 @@
1
+ import { describe, it, expect, beforeEach } from "vitest"
2
+ import { tel, setLocale, validateTaiwanTel } from "../../src"
3
+
4
+ describe("Taiwan tel() validator", () => {
5
+ beforeEach(() => setLocale("en"))
6
+
7
+ describe("basic functionality", () => {
8
+ it("should validate correct Taiwan landline telephone numbers", () => {
9
+ const schema = tel()
10
+
11
+ // Valid Taiwan landline numbers (various formats)
12
+ // Taipei (02) - requires 10 digits total, first digit after 02 must be 2,3,5-8
13
+ expect(schema.parse("0223456789")).toBe("0223456789") // 02-2345-6789 (10 digits)
14
+ expect(schema.parse("0232345678")).toBe("0232345678") // 02-3234-5678 (10 digits)
15
+
16
+ // Kaohsiung (07) - requires 9 digits total, the first digit after 07 must be 2-9
17
+ expect(schema.parse("072345678")).toBe("072345678") // 07-234-5678 (9 digits)
18
+ expect(schema.parse("073456789")).toBe("073456789") // 07-345-6789 (9 digits)
19
+
20
+ // Taichung (04) - requires 9 digits total
21
+ expect(schema.parse("041234567")).toBe("041234567") // 04-123-4567 (9 digits)
22
+ expect(schema.parse("043456789")).toBe("043456789") // 04-345-6789 (9 digits)
23
+
24
+ // Tainan (06) - requires 9 digits total
25
+ expect(schema.parse("061234567")).toBe("061234567") // 06-123-4567 (9 digits)
26
+ expect(schema.parse("063456789")).toBe("063456789") // 06-345-6789 (9 digits)
27
+
28
+ // Other areas
29
+ expect(schema.parse("031234567")).toBe("031234567") // 03-123-4567 (9 digits)
30
+ expect(schema.parse("051234567")).toBe("051234567") // 05-123-4567 (9 digits)
31
+ expect(schema.parse("084234567")).toBe("084234567") // 08-423-4567 (9 digits, 08 area code with 4)
32
+ expect(schema.parse("087234567")).toBe("087234567") // 08-723-4567 (9 digits, 08 area code with 7)
33
+
34
+ // 3-digit area codes
35
+ expect(schema.parse("082234567")).toBe("082234567") // 082-234567 (9 digits, Kinmen)
36
+ expect(schema.parse("089234567")).toBe("089234567") // 089-234567 (9 digits, Taitung)
37
+
38
+ // 4-digit area codes
39
+ expect(schema.parse("082661234")).toBe("082661234") // 0826-61234 (9 digits, Wuqiu)
40
+ expect(schema.parse("083621234")).toBe("083621234") // 0836-21234 (9 digits, Matsu)
41
+ })
42
+
43
+ it("should validate numbers with separators", () => {
44
+ const schema = tel()
45
+
46
+ // Numbers with dashes
47
+ expect(schema.parse("02-2345-6789")).toBe("02-2345-6789")
48
+ expect(schema.parse("07-234-5678")).toBe("07-234-5678")
49
+ expect(schema.parse("082-234567")).toBe("082-234567")
50
+
51
+ // Numbers with spaces
52
+ expect(schema.parse("02 2345 6789")).toBe("02 2345 6789")
53
+ expect(schema.parse("07 234 5678")).toBe("07 234 5678")
54
+ expect(schema.parse("082 234567")).toBe("082 234567")
55
+
56
+ // Mixed separators
57
+ expect(schema.parse("02-2345 6789")).toBe("02-2345 6789")
58
+ expect(schema.parse("07 234-5678")).toBe("07 234-5678")
59
+ })
60
+
61
+ it("should reject invalid Taiwan telephone numbers", () => {
62
+ const schema = tel()
63
+
64
+ // Invalid formats
65
+ expect(() => schema.parse("123456789")).toThrow("Invalid Taiwan telephone format") // Missing leading 0
66
+ expect(() => schema.parse("01234567890")).toThrow("Invalid Taiwan telephone format") // Too long
67
+ expect(() => schema.parse("0123456")).toThrow("Invalid Taiwan telephone format") // Too short
68
+ expect(() => schema.parse("1012345678")).toThrow("Invalid Taiwan telephone format") // Wrong prefix (10)
69
+ expect(() => schema.parse("")).toThrow("Required")
70
+ expect(() => schema.parse("abcdefghij")).toThrow("Invalid Taiwan telephone format")
71
+
72
+ // Invalid area codes
73
+ expect(() => schema.parse("0012345678")).toThrow("Invalid Taiwan telephone format") // 00 not valid
74
+ expect(() => schema.parse("0112345678")).toThrow("Invalid Taiwan telephone format") // 01 not valid
75
+ expect(() => schema.parse("0912345678")).toThrow("Invalid Taiwan telephone format") // 09 is mobile, not landline
76
+
77
+ // Invalid 3-digit codes
78
+ expect(() => schema.parse("081123456")).toThrow("Invalid Taiwan telephone format") // 081 not valid
79
+ expect(() => schema.parse("083123456")).toThrow("Invalid Taiwan telephone format") // 083 not valid
80
+
81
+ // Wrong length for valid area codes
82
+ expect(() => schema.parse("02123456789")).toThrow("Invalid Taiwan telephone format") // Too long for 02
83
+ expect(() => schema.parse("82123456")).toThrow("Invalid Taiwan telephone format") // Missing leading 0
84
+ })
85
+
86
+ it("should handle whitespace trimming", () => {
87
+ const schema = tel()
88
+
89
+ expect(schema.parse(" 0223456789 ")).toBe("0223456789")
90
+ expect(schema.parse("\t072345678\n")).toBe("072345678")
91
+ })
92
+ })
93
+
94
+ describe("whitelist functionality", () => {
95
+ it("should accept any string in whitelist regardless of format", () => {
96
+ const schema = tel({
97
+ whitelist: ["custom-tel", "emergency-line", "0912345678"],
98
+ })
99
+
100
+ // Allowlist entries should be accepted even if they don't match Taiwan telephone formats
101
+ expect(schema.parse("custom-tel")).toBe("custom-tel")
102
+ expect(schema.parse("emergency-line")).toBe("emergency-line")
103
+ expect(schema.parse("0912345678")).toBe("0912345678") // Mobile number but in allowlist
104
+
105
+ // Valid telephone numbers not in the allowlist should be rejected
106
+ expect(() => schema.parse("0212345678")).toThrow("Not in allowed telephone list")
107
+ expect(() => schema.parse("0711111111")).toThrow("Not in allowed telephone list")
108
+ })
109
+
110
+ it("should reject values not in whitelist when whitelist is provided", () => {
111
+ const schema = tel({
112
+ whitelist: ["allowed-value", "0223456789"],
113
+ })
114
+
115
+ expect(() => schema.parse("0312345678")).toThrow("Not in allowed telephone list")
116
+ expect(() => schema.parse("invalid-value")).toThrow("Not in allowed telephone list")
117
+ })
118
+
119
+ it("should work with empty whitelist", () => {
120
+ const schema = tel({
121
+ whitelist: [],
122
+ })
123
+
124
+ // With empty allowlist, should still validate a telephone format
125
+ expect(schema.parse("0223456789")).toBe("0223456789")
126
+ expect(() => schema.parse("0912345678")).toThrow("Invalid Taiwan telephone format")
127
+ })
128
+
129
+ it("should prioritize whitelist over format validation", () => {
130
+ const schema = tel({
131
+ required: false,
132
+ whitelist: ["not-a-phone", "123", ""],
133
+ })
134
+
135
+ // These should be accepted despite being invalid telephone formats
136
+ expect(schema.parse("not-a-phone")).toBe("not-a-phone")
137
+ expect(schema.parse("123")).toBe("123")
138
+ expect(schema.parse("")).toBe("")
139
+ })
140
+ })
141
+
142
+ describe("required/optional behavior", () => {
143
+ it("should handle required=true (default)", () => {
144
+ const schema = tel()
145
+
146
+ expect(() => schema.parse("")).toThrow("Required")
147
+ expect(() => schema.parse(null)).toThrow()
148
+ expect(() => schema.parse(undefined)).toThrow()
149
+ })
150
+
151
+ it("should handle required=false", () => {
152
+ const schema = tel({ required: false })
153
+
154
+ expect(schema.parse("")).toBe(null)
155
+ expect(schema.parse(null)).toBe(null)
156
+ expect(schema.parse(undefined)).toBe(null)
157
+ expect(schema.parse("0223456789")).toBe("0223456789")
158
+ })
159
+
160
+ it("should use default values", () => {
161
+ const requiredSchema = tel({ defaultValue: "0223456789" })
162
+ const optionalSchema = tel({ required: false, defaultValue: "0223456789" })
163
+
164
+ expect(requiredSchema.parse("")).toBe("0223456789")
165
+ expect(optionalSchema.parse("")).toBe("0223456789")
166
+ })
167
+
168
+ it("should handle whitelist with optional fields", () => {
169
+ const schema = tel({
170
+ required: false,
171
+ whitelist: ["custom-value", "0223456789"],
172
+ })
173
+
174
+ expect(schema.parse("")).toBe(null)
175
+ expect(schema.parse("custom-value")).toBe("custom-value")
176
+ expect(schema.parse("0223456789")).toBe("0223456789")
177
+ expect(() => schema.parse("0312345678")).toThrow("Not in allowed telephone list")
178
+ })
179
+ })
180
+
181
+ describe("transform function", () => {
182
+ it("should apply custom transform", () => {
183
+ const schema = tel({
184
+ transform: (val) => val.replace(/[-\s]/g, ""),
185
+ })
186
+
187
+ expect(schema.parse("02-2345-6789")).toBe("0223456789")
188
+ expect(schema.parse("07 234 5678")).toBe("072345678")
189
+ })
190
+
191
+ it("should apply transform before validation", () => {
192
+ const schema = tel({
193
+ transform: (val) => val.replace(/\s+/g, ""),
194
+ })
195
+
196
+ expect(schema.parse(" 02 234 5678 9 ")).toBe("0223456789")
197
+ expect(() => schema.parse(" 09 123 4567 8 ")).toThrow("Invalid Taiwan telephone format")
198
+ })
199
+
200
+ it("should work with whitelist after transform", () => {
201
+ const schema = tel({
202
+ transform: (val) => val.replace(/[-\s]/g, ""),
203
+ whitelist: ["0223456789", "customvalue"],
204
+ })
205
+
206
+ expect(schema.parse("customvalue")).toBe("customvalue")
207
+ expect(() => schema.parse("03-123-4567")).toThrow("Not in allowed telephone list")
208
+ })
209
+ })
210
+
211
+ describe("input preprocessing", () => {
212
+ it("should handle string conversion", () => {
213
+ const schema = tel()
214
+
215
+ // Test string conversion of numbers
216
+ expect(() => schema.parse(212345678)).toThrow("Invalid Taiwan telephone format") // Invalid because missing leading 0
217
+ })
218
+
219
+ it("should trim whitespace", () => {
220
+ const schema = tel()
221
+
222
+ expect(schema.parse(" 0223456789 ")).toBe("0223456789")
223
+ expect(schema.parse("\t072345678\n")).toBe("072345678")
224
+ })
225
+ })
226
+
227
+ describe("utility function", () => {
228
+ describe("validateTaiwanTel", () => {
229
+ it("should correctly validate Taiwan telephone numbers", () => {
230
+ // Valid numbers
231
+ expect(validateTaiwanTel("0223456789")).toBe(true) // Taipei
232
+ expect(validateTaiwanTel("072345678")).toBe(true) // Kaohsiung
233
+ expect(validateTaiwanTel("041234567")).toBe(true) // Taichung
234
+ expect(validateTaiwanTel("037234567")).toBe(true) // Miaoli
235
+ expect(validateTaiwanTel("0492345678")).toBe(true) // Nantou
236
+ expect(validateTaiwanTel("084234567")).toBe(true) // Pingtung
237
+ expect(validateTaiwanTel("082234567")).toBe(true) // Kinmen
238
+ expect(validateTaiwanTel("089234567")).toBe(true) // Taitung
239
+ expect(validateTaiwanTel("082661234")).toBe(true) // Wuqiu
240
+ expect(validateTaiwanTel("083621234")).toBe(true) // Matsu
241
+
242
+ // Valid with separators
243
+ expect(validateTaiwanTel("02-2345-6789")).toBe(true)
244
+ expect(validateTaiwanTel("07 234 5678")).toBe(true)
245
+ expect(validateTaiwanTel("082-234567")).toBe(true)
246
+
247
+ // Invalid numbers
248
+ expect(validateTaiwanTel("0912345678")).toBe(false) // Mobile prefix
249
+ expect(validateTaiwanTel("1212345678")).toBe(false) // Wrong prefix
250
+ expect(validateTaiwanTel("02123456")).toBe(false) // Too short for 02
251
+ expect(validateTaiwanTel("021234567890")).toBe(false) // Too long
252
+ expect(validateTaiwanTel("212345678")).toBe(false) // Missing leading 0
253
+ expect(validateTaiwanTel("")).toBe(false) // Empty
254
+ expect(validateTaiwanTel("abcdefghij")).toBe(false) // Non-numeric
255
+ expect(validateTaiwanTel("081123456")).toBe(false) // Invalid 3-digit area code
256
+ expect(validateTaiwanTel("0214567890")).toBe(false) // Invalid first digit for 02
257
+ expect(validateTaiwanTel("071234567")).toBe(false) // Invalid first digit for 07
258
+ expect(validateTaiwanTel("037134567")).toBe(false) // Invalid first digit for 037
259
+ expect(validateTaiwanTel("081234567")).toBe(false) // Invalid first digit for 08
260
+ })
261
+ })
262
+ })
263
+
264
+ describe("i18n support", () => {
265
+ it("should use English messages by default", () => {
266
+ setLocale("en")
267
+ const schema = tel()
268
+
269
+ expect(() => schema.parse("")).toThrow("Required")
270
+ expect(() => schema.parse("0912345678")).toThrow("Invalid Taiwan telephone format")
271
+ })
272
+
273
+ it("should use Chinese messages when locale is zh-TW", () => {
274
+ setLocale("zh-TW")
275
+ const schema = tel()
276
+
277
+ expect(() => schema.parse("")).toThrow("必填")
278
+ expect(() => schema.parse("0912345678")).toThrow("無效的市話號碼格式")
279
+ })
280
+
281
+ it("should support whitelist error messages", () => {
282
+ setLocale("en")
283
+ const schema = tel({
284
+ whitelist: ["0212345678"],
285
+ })
286
+
287
+ expect(() => schema.parse("0312345678")).toThrow("Not in allowed telephone list")
288
+
289
+ setLocale("zh-TW")
290
+ expect(() => schema.parse("0312345678")).toThrow("不在允許的市話號碼清單中")
291
+ })
292
+
293
+ it("should support custom i18n messages", () => {
294
+ const schema = tel({
295
+ i18n: {
296
+ en: {
297
+ required: "Telephone number is required",
298
+ invalid: "Telephone number format is invalid",
299
+ notInWhitelist: "Telephone number not allowed",
300
+ },
301
+ "zh-TW": {
302
+ required: "請輸入電話號碼",
303
+ invalid: "電話號碼格式錯誤",
304
+ notInWhitelist: "電話號碼不被允許",
305
+ },
306
+ },
307
+ })
308
+
309
+ setLocale("en")
310
+ expect(() => schema.parse("")).toThrow("Telephone number is required")
311
+ expect(() => schema.parse("0912345678")).toThrow("Telephone number format is invalid")
312
+
313
+ setLocale("zh-TW")
314
+ expect(() => schema.parse("")).toThrow("請輸入電話號碼")
315
+ expect(() => schema.parse("0912345678")).toThrow("電話號碼格式錯誤")
316
+ })
317
+
318
+ it("should support custom whitelist messages", () => {
319
+ const schema = tel({
320
+ whitelist: ["0212345678"],
321
+ i18n: {
322
+ en: {
323
+ notInWhitelist: "This telephone number is not allowed",
324
+ },
325
+ "zh-TW": {
326
+ notInWhitelist: "此電話號碼不被允許",
327
+ },
328
+ },
329
+ })
330
+
331
+ setLocale("en")
332
+ expect(() => schema.parse("0312345678")).toThrow("This telephone number is not allowed")
333
+
334
+ setLocale("zh-TW")
335
+ expect(() => schema.parse("0312345678")).toThrow("此電話號碼不被允許")
336
+ })
337
+ })
338
+
339
+ describe("real world Taiwan telephone numbers", () => {
340
+ it("should validate all major area codes", () => {
341
+ const schema = tel()
342
+
343
+ // Test Taiwan landline area codes using exact working numbers from a utility test
344
+ const validAreaCodes = [
345
+ { code: "02", numbers: ["0223456789"] }, // Taipei (10 digits, first digit 2) - from utility test ✓
346
+ { code: "04", numbers: ["041234567"] }, // Taichung (9 digits) - from utility test ✓
347
+ { code: "037", numbers: ["037234567"] }, // Miaoli (9 digits, first digit 2) - from utility test ✓
348
+ { code: "049", numbers: ["0492345678"] }, // Nantou (10 digits, first digit 2) - from utility test ✓
349
+ { code: "07", numbers: ["072345678"] }, // Kaohsiung (9 digits, first digit 2) - from utility test ✓
350
+ { code: "08", numbers: ["084234567"] }, // Pingtung (9 digits, first digit 4) - from utility test ✓
351
+ { code: "082", numbers: ["082234567"] }, // Kinmen (9 digits, first digit 2) - from utility test ✓
352
+ { code: "089", numbers: ["089234567"] }, // Taitung (9 digits, first digit 2) - from utility test ✓
353
+ { code: "0826", numbers: ["082661234"] }, // Wuqiu (9 digits, first digit 6) - from utility test ✓
354
+ { code: "0836", numbers: ["083621234"] }, // Matsu (9 digits, first digit 2) - from utility test ✓
355
+ ]
356
+
357
+ validAreaCodes.forEach(({ numbers }) => {
358
+ numbers.forEach((number) => {
359
+ expect(schema.parse(number)).toBe(number)
360
+ })
361
+ })
362
+ })
363
+
364
+ it("should reject mobile phone prefixes", () => {
365
+ const schema = tel()
366
+
367
+ // Test invalid mobile prefixes (090-099)
368
+ const mobilePrefixes = ["090", "091", "092", "093", "094", "095", "096", "097", "098", "099"]
369
+
370
+ mobilePrefixes.forEach((prefix) => {
371
+ const phoneNumber = prefix + "1234567"
372
+ expect(() => schema.parse(phoneNumber)).toThrow("Invalid Taiwan telephone format")
373
+ })
374
+ })
375
+
376
+ it("should validate realistic landline number patterns", () => {
377
+ const schema = tel()
378
+
379
+ const realLandlineNumbers = [
380
+ "0223456789", // Taipei 10-digit (valid first digit 2) - from utility test ✓
381
+ "072345678", // Kaohsiung 9-digit (valid first digit 2) - from utility test ✓
382
+ "041234567", // Taichung 9-digit - from utility test ✓
383
+ "037234567", // Miaoli 9-digit (valid first digit 2) - from utility test ✓
384
+ "0492345678", // Nantou 10-digit (valid first digit 2) - from utility test ✓
385
+ "084234567", // Pingtung 9-digit (valid first digit 4) - from utility test ✓
386
+ "082234567", // Kinmen 9-digit (valid first digit 2) - from utility test ✓
387
+ "089234567", // Taitung 9-digit (valid first digit 2) - from utility test ✓
388
+ "082661234", // Wuqiu 9-digit (valid first digit 6) - from utility test ✓
389
+ "083621234", // Matsu (valid) - from utility test ✓
390
+ ]
391
+
392
+ realLandlineNumbers.forEach((phone) => {
393
+ expect(schema.parse(phone)).toBe(phone)
394
+ })
395
+ })
396
+ })
397
+
398
+ describe("edge cases", () => {
399
+ it("should handle various input types", () => {
400
+ const schema = tel()
401
+
402
+ // Test different input types that should be converted to string
403
+ expect(schema.parse("0223456789")).toBe("0223456789")
404
+ })
405
+
406
+ it("should handle empty and whitespace inputs", () => {
407
+ const schema = tel()
408
+ const optionalSchema = tel({ required: false })
409
+
410
+ expect(() => schema.parse("")).toThrow("Required")
411
+ expect(() => schema.parse(" ")).toThrow("Required")
412
+ expect(() => schema.parse("\t\n")).toThrow("Required")
413
+
414
+ expect(optionalSchema.parse("")).toBe(null)
415
+ expect(optionalSchema.parse(" ")).toBe(null)
416
+ expect(optionalSchema.parse("\t\n")).toBe(null)
417
+ })
418
+
419
+ it("should preserve valid format after transformation", () => {
420
+ const schema = tel({
421
+ transform: (val) => val.replace(/[^0-9]/g, ""),
422
+ })
423
+
424
+ expect(schema.parse("02-234-56789")).toBe("0223456789")
425
+ expect(schema.parse("07 234 5678")).toBe("072345678")
426
+ // Test with letters that should be filtered out, leaving a valid number
427
+ expect(schema.parse("02abc234def56789")).toBe("0223456789")
428
+ })
429
+
430
+ it("should work with complex whitelist scenarios", () => {
431
+ const schema = tel({
432
+ whitelist: ["0223456789", "emergency", "custom-contact-123", ""],
433
+ required: false,
434
+ })
435
+
436
+ // Allowlist scenarios
437
+ expect(schema.parse("0223456789")).toBe("0223456789")
438
+ expect(schema.parse("emergency")).toBe("emergency")
439
+ expect(schema.parse("custom-contact-123")).toBe("custom-contact-123")
440
+ expect(schema.parse("")).toBe("")
441
+
442
+ // Not in the allowlist
443
+ expect(() => schema.parse("0312345678")).toThrow("Not in allowed telephone list")
444
+ expect(() => schema.parse("other-value")).toThrow("Not in allowed telephone list")
445
+ })
446
+
447
+ it("should handle boundary cases for area codes", () => {
448
+ const schema = tel()
449
+
450
+ // Test minimum and maximum valid lengths for different area codes
451
+ // 2-digit area codes: 9-10 digits total
452
+ expect(schema.parse("0223456789")).toBe("0223456789") // 10 digits (02 area requires 10)
453
+ expect(() => schema.parse("02123456")).toThrow("Invalid Taiwan telephone format") // 8 digits (too short)
454
+ expect(() => schema.parse("021234567890")).toThrow("Invalid Taiwan telephone format") // 12 digits (too long)
455
+
456
+ // 3-digit area codes: 9 digits total
457
+ expect(schema.parse("082234567")).toBe("082234567") // 9 digits (082 area, first digit 2)
458
+ expect(() => schema.parse("08212345")).toThrow("Invalid Taiwan telephone format") // 8 digits (too short)
459
+ expect(() => schema.parse("0821234567")).toThrow("Invalid Taiwan telephone format") // 10 digits (too long)
460
+
461
+ // 4-digit area codes: 9 digits total
462
+ expect(schema.parse("082661234")).toBe("082661234") // 9 digits (0826 + 6 + 4 digits)
463
+ expect(() => schema.parse("08261234")).toThrow("Invalid Taiwan telephone format") // 8 digits (too short)
464
+ expect(() => schema.parse("0826123456")).toThrow("Invalid Taiwan telephone format") // 10 digits (too long)
465
+ })
466
+ })
467
+ })
package/eslint.config.mjs DELETED
@@ -1,10 +0,0 @@
1
- // @ts-check
2
-
3
- import eslint from "@eslint/js"
4
- import tseslint from "typescript-eslint"
5
-
6
- export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended, {
7
- rules: {
8
- "@typescript-eslint/no-explicit-any": "off",
9
- },
10
- })
@@ -1,36 +0,0 @@
1
- import { z, ZodBoolean, ZodNullable, ZodType } from "zod"
2
- import { t } from "../i18n"
3
-
4
- export type BooleanOptions<IsRequired extends boolean = true> = {
5
- required?: IsRequired
6
- defaultValue?: IsRequired extends true ? boolean : boolean | null
7
- shouldBe?: boolean
8
- }
9
-
10
- export type BooleanSchema<IsRequired extends boolean> = IsRequired extends true ? ZodBoolean : ZodNullable<ZodBoolean>
11
-
12
- export function boolean<IsRequired extends boolean = true>(options?: BooleanOptions<IsRequired>): BooleanSchema<IsRequired> {
13
- const { required = true, defaultValue = null, shouldBe } = options ?? {}
14
-
15
- let result: ZodType = z.preprocess(
16
- (val) => {
17
- if (val === "" || val === undefined || val === null) return defaultValue
18
- if (val === "true" || val === 1 || val === "1") return true
19
- if (val === "false" || val === 0 || val === "0") return false
20
- return val
21
- },
22
- z.union([z.literal(true), z.literal(false), z.literal(null)])
23
- )
24
-
25
- if (required && defaultValue === null) {
26
- result = result.refine((val) => val !== null, { message: t("common.boolean.required") })
27
- }
28
-
29
- if (shouldBe === true) {
30
- result = result.refine((val) => val === true, { message: t("common.boolean.shouldBe.true") })
31
- } else if (shouldBe === false) {
32
- result = result.refine((val) => val === false, { message: t("common.boolean.shouldBe.false") })
33
- }
34
-
35
- return result as IsRequired extends true ? ZodBoolean : ZodNullable<ZodBoolean>
36
- }
@@ -1,43 +0,0 @@
1
- import { z, ZodNullable, ZodString } from "zod"
2
- import { t } from "../i18n"
3
- import dayjs from "dayjs"
4
- import customParseFormat from "dayjs/plugin/customParseFormat"
5
- import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
6
- import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
7
-
8
- dayjs.extend(isSameOrAfter)
9
- dayjs.extend(isSameOrBefore)
10
- dayjs.extend(customParseFormat)
11
-
12
- export type DateOptions<IsRequired extends boolean = true> = {
13
- required?: IsRequired
14
- min?: number
15
- max?: number
16
- format?: string
17
- includes?: string
18
- defaultValue?: IsRequired extends true ? string : string | null
19
- }
20
-
21
- export type DateSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
22
-
23
- export function date<IsRequired extends boolean = true>(options?: DateOptions<IsRequired>): DateSchema<IsRequired> {
24
- const { required = true, min, max, format = "YYYY-MM-DD", includes, defaultValue = null } = options ?? {}
25
-
26
- const baseSchema = required
27
- ? z.preprocess((val) => (val === "" || val === null || val === undefined ? defaultValue : val), z.coerce.string().trim())
28
- : z.preprocess((val) => (val === "" || val === null || val === undefined ? defaultValue : val), z.coerce.string().trim().nullable())
29
-
30
- const schema = baseSchema
31
- .refine(
32
- (val) => {
33
- if (!val) return !required
34
- return dayjs(val, format, true).isValid()
35
- },
36
- { message: t("common.date.format", { format }) }
37
- )
38
- .refine((val) => val === null || min === undefined || dayjs(val, format).isSameOrAfter(dayjs(min, format)), { message: t("common.date.min", { min }) })
39
- .refine((val) => val === null || max === undefined || dayjs(val, format).isSameOrBefore(dayjs(max, format)), { message: t("common.date.max", { max }) })
40
- .refine((val) => val === null || includes === undefined || val.includes(includes), { message: t("common.date.includes", { includes }) })
41
-
42
- return schema as unknown as DateSchema<IsRequired>
43
- }
@@ -1,44 +0,0 @@
1
- import { z, ZodNullable, ZodString } from "zod"
2
- import { t } from "../i18n"
3
- import { TextSchema } from "./text"
4
-
5
- export type EmailOptions<IsRequired extends boolean = true> = {
6
- required?: IsRequired
7
- domain?: string
8
- min?: number
9
- max?: number
10
- includes?: string
11
- }
12
-
13
- export type EmailSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
14
-
15
- export function email<IsRequired extends boolean = true>(options?: EmailOptions<IsRequired>): EmailSchema<IsRequired> {
16
- const { required = true, domain, min, max, includes } = options ?? {}
17
-
18
- const baseSchema = required
19
- ? z.preprocess(
20
- (val) => (val === "" || val === null || val === undefined ? null : val),
21
- z.email({
22
- error: (issue) => {
23
- if (issue.code === "invalid_type") return t("common.email.required")
24
- else if (issue.code === "invalid_format") return t("common.email.invalid")
25
- return t("common.email.invalid")
26
- },
27
- })
28
- )
29
- : z.preprocess((val) => (val === "" || val === null || val === undefined ? null : val), z.email({ message: t("common.email.invalid") }).nullable())
30
-
31
- const schema = baseSchema
32
- .refine((val) => (required ? val !== "" && val !== "null" && val !== "undefined" : true), { message: t("common.text.required") })
33
- .refine((val) => val === null || min === undefined || val.length >= min, { message: t("common.text.min", { min }) })
34
- .refine((val) => val === null || max === undefined || val.length <= max, { message: t("common.text.max", { max }) })
35
- .refine((val) => val === null || includes === undefined || val.includes(includes), { message: t("common.text.includes", { includes }) })
36
- .refine(
37
- (val) => {
38
- if (val === null || domain === undefined) return true
39
- return val.split("@")[1]?.toLowerCase() === domain.toLowerCase()
40
- },
41
- { message: t("common.email.domain", { domain }) }
42
- )
43
- return schema as unknown as TextSchema<IsRequired>
44
- }
@@ -1,46 +0,0 @@
1
- import { z, ZodNullable, ZodNumber } from "zod"
2
- import { t } from "../i18n"
3
-
4
- export type IntegerOptions<IsRequired extends boolean = true> = {
5
- required?: IsRequired
6
- min?: number
7
- max?: number
8
- defaultValue?: IsRequired extends true ? number : number | null
9
- }
10
-
11
- export type IntegerSchema<IsRequired extends boolean> = IsRequired extends true ? ZodNumber : ZodNullable<ZodNumber>
12
-
13
- export function integer<IsRequired extends boolean = true>(options?: IntegerOptions<IsRequired>): IntegerSchema<IsRequired> {
14
- const { required = true, min, max, defaultValue } = options ?? {}
15
-
16
- const schema = z
17
- .preprocess(
18
- (val) => {
19
- if (val === "" || val === undefined || val === null) return defaultValue ?? null
20
- return typeof val === "string" ? Number(val) : val
21
- },
22
- z.union([
23
- z.number({
24
- error: (issue) => {
25
- if (issue.code === "invalid_type") return t("common.integer.integer")
26
- return t("common.integer.required")
27
- },
28
- }),
29
- z.null(),
30
- ])
31
- )
32
- .refine((val) => !required || val !== null, {
33
- message: t("common.integer.required"),
34
- })
35
- .refine((val) => val === null || Number.isInteger(val), {
36
- message: t("common.integer.integer"),
37
- })
38
- .refine((val) => val === null || min === undefined || val >= min, {
39
- message: t("common.integer.min", { min }),
40
- })
41
- .refine((val) => val === null || max === undefined || val <= max, {
42
- message: t("common.integer.max", { max }),
43
- })
44
-
45
- return schema as unknown as IntegerSchema<IsRequired>
46
- }