@hy_ong/zod-kit 0.0.2 → 0.0.5

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 (51) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +7 -7
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +1663 -189
  7. package/dist/index.d.cts +324 -32
  8. package/dist/index.d.ts +324 -32
  9. package/dist/index.js +1634 -187
  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 +123 -49
  14. package/src/i18n/locales/zh-TW.json +123 -46
  15. package/src/index.ts +13 -7
  16. package/src/validators/common/boolean.ts +97 -0
  17. package/src/validators/common/date.ts +171 -0
  18. package/src/validators/common/email.ts +200 -0
  19. package/src/validators/common/id.ts +259 -0
  20. package/src/validators/common/number.ts +194 -0
  21. package/src/validators/common/password.ts +214 -0
  22. package/src/validators/common/text.ts +151 -0
  23. package/src/validators/common/url.ts +207 -0
  24. package/src/validators/taiwan/business-id.ts +140 -0
  25. package/src/validators/taiwan/fax.ts +182 -0
  26. package/src/validators/taiwan/mobile.ts +110 -0
  27. package/src/validators/taiwan/national-id.ts +208 -0
  28. package/src/validators/taiwan/tel.ts +182 -0
  29. package/tests/common/boolean.test.ts +340 -92
  30. package/tests/common/date.test.ts +458 -0
  31. package/tests/common/email.test.ts +232 -60
  32. package/tests/common/id.test.ts +535 -0
  33. package/tests/common/number.test.ts +230 -60
  34. package/tests/common/password.test.ts +281 -54
  35. package/tests/common/text.test.ts +227 -30
  36. package/tests/common/url.test.ts +492 -67
  37. package/tests/taiwan/business-id.test.ts +240 -0
  38. package/tests/taiwan/fax.test.ts +463 -0
  39. package/tests/taiwan/mobile.test.ts +373 -0
  40. package/tests/taiwan/national-id.test.ts +435 -0
  41. package/tests/taiwan/tel.test.ts +467 -0
  42. package/eslint.config.mjs +0 -10
  43. package/src/common/boolean.ts +0 -37
  44. package/src/common/date.ts +0 -44
  45. package/src/common/email.ts +0 -45
  46. package/src/common/integer.ts +0 -47
  47. package/src/common/number.ts +0 -38
  48. package/src/common/password.ts +0 -34
  49. package/src/common/text.ts +0 -35
  50. package/src/common/url.ts +0 -38
  51. package/tests/common/integer.test.ts +0 -90
@@ -0,0 +1,435 @@
1
+ import { describe, it, expect, beforeEach } from "vitest"
2
+ import { nationalId, setLocale, validateTaiwanNationalId, validateCitizenId, validateOldResidentId, validateNewResidentId } from "../../src"
3
+
4
+ describe("Taiwan nationalId() validator", () => {
5
+ beforeEach(() => setLocale("en"))
6
+
7
+ describe("basic functionality", () => {
8
+ it("should validate correct Taiwan national IDs (both types)", () => {
9
+ const schema = nationalId()
10
+
11
+ // Valid citizen IDs (身分證字號)
12
+ expect(schema.parse("A123456789")).toBe("A123456789")
13
+ expect(schema.parse("A223456781")).toBe("A223456781")
14
+ expect(schema.parse("B123456780")).toBe("B123456780")
15
+
16
+ // Valid old resident IDs (舊式居留證)
17
+ expect(schema.parse("AA00000001")).toBe("AA00000001")
18
+ expect(schema.parse("AB00000009")).toBe("AB00000009")
19
+ expect(schema.parse("BC00000002")).toBe("BC00000002")
20
+
21
+ // Valid new resident IDs (新式居留證)
22
+ expect(schema.parse("A800000005")).toBe("A800000005")
23
+ expect(schema.parse("A900000007")).toBe("A900000007")
24
+ expect(schema.parse("B800000006")).toBe("B800000006")
25
+ })
26
+
27
+ it("should reject invalid Taiwan national IDs", () => {
28
+ const schema = nationalId()
29
+
30
+ // Invalid formats
31
+ expect(() => schema.parse("A12345678")).toThrow("Invalid Taiwan National ID") // Too short
32
+ expect(() => schema.parse("A1234567890")).toThrow("Invalid Taiwan National ID") // Too long
33
+ expect(() => schema.parse("1234567890")).toThrow("Invalid Taiwan National ID") // No letter
34
+ expect(() => schema.parse("AB23456789")).toThrow("Invalid Taiwan National ID") // Wrong format
35
+
36
+ // Invalid checksums
37
+ expect(() => schema.parse("A123456788")).toThrow("Invalid Taiwan National ID")
38
+ expect(() => schema.parse("A223456782")).toThrow("Invalid Taiwan National ID")
39
+ })
40
+
41
+ it("should handle case conversion", () => {
42
+ const schema = nationalId()
43
+
44
+ expect(schema.parse("a123456789")).toBe("A123456789")
45
+ expect(schema.parse("b123456780")).toBe("B123456780")
46
+ })
47
+ })
48
+
49
+ describe("type-specific validation", () => {
50
+ it("should validate only citizen IDs when type is 'citizen'", () => {
51
+ const schema = nationalId({ type: "citizen" })
52
+
53
+ // Should accept citizen IDs
54
+ expect(schema.parse("A123456789")).toBe("A123456789")
55
+ expect(schema.parse("B123456780")).toBe("B123456780")
56
+
57
+ // Should reject resident IDs
58
+ expect(() => schema.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
59
+ expect(() => schema.parse("A800000005")).toThrow("Invalid Taiwan National ID")
60
+ })
61
+
62
+ it("should validate only resident IDs when type is 'resident'", () => {
63
+ const schema = nationalId({ type: "resident" })
64
+
65
+ // Should accept old resident IDs
66
+ expect(schema.parse("AA00000001")).toBe("AA00000001")
67
+ expect(schema.parse("ZB12345676")).toBe("ZB12345676")
68
+
69
+ // Should accept new resident IDs
70
+ expect(schema.parse("A800000005")).toBe("A800000005")
71
+ expect(schema.parse("Z812345672")).toBe("Z812345672")
72
+
73
+ // Should reject citizen IDs
74
+ expect(() => schema.parse("A123456789")).toThrow("Invalid Taiwan National ID")
75
+ expect(() => schema.parse("B123456780")).toThrow("Invalid Taiwan National ID")
76
+ })
77
+
78
+ it("should validate both types when type is 'both' (default)", () => {
79
+ const schema = nationalId({ type: "both" })
80
+
81
+ // Should accept all valid formats
82
+ expect(schema.parse("A123456789")).toBe("A123456789") // Citizen
83
+ expect(schema.parse("AA00000001")).toBe("AA00000001") // Old resident
84
+ expect(schema.parse("A800000005")).toBe("A800000005") // New resident
85
+ })
86
+ })
87
+
88
+ describe("allowOldResident option", () => {
89
+ it("should allow old resident IDs by default", () => {
90
+ const schema = nationalId({ type: "resident" })
91
+
92
+ // Should accept old resident IDs (default allowOldResident=true)
93
+ expect(schema.parse("AA00000001")).toBe("AA00000001")
94
+ expect(schema.parse("AB00000009")).toBe("AB00000009")
95
+ expect(schema.parse("ZB12345676")).toBe("ZB12345676")
96
+
97
+ // Should also accept new resident IDs
98
+ expect(schema.parse("A800000005")).toBe("A800000005")
99
+ expect(schema.parse("A912345673")).toBe("A912345673")
100
+ })
101
+
102
+ it("should reject old resident IDs when allowOldResident=false", () => {
103
+ const schema = nationalId({ type: "resident", allowOldResident: false })
104
+
105
+ // Should reject old resident IDs
106
+ expect(() => schema.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
107
+ expect(() => schema.parse("AB00000009")).toThrow("Invalid Taiwan National ID")
108
+ expect(() => schema.parse("ZB12345676")).toThrow("Invalid Taiwan National ID")
109
+
110
+ // Should still accept new resident IDs
111
+ expect(schema.parse("A800000005")).toBe("A800000005")
112
+ expect(schema.parse("A912345673")).toBe("A912345673")
113
+ })
114
+
115
+ it("should work with type='both' and allowOldResident=false", () => {
116
+ const schema = nationalId({ type: "both", allowOldResident: false })
117
+
118
+ // Should accept citizen IDs
119
+ expect(schema.parse("A123456789")).toBe("A123456789")
120
+ expect(schema.parse("B123456780")).toBe("B123456780")
121
+
122
+ // Should accept new resident IDs
123
+ expect(schema.parse("A800000005")).toBe("A800000005")
124
+ expect(schema.parse("A912345673")).toBe("A912345673")
125
+
126
+ // Should reject old resident IDs
127
+ expect(() => schema.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
128
+ expect(() => schema.parse("AB00000009")).toThrow("Invalid Taiwan National ID")
129
+ })
130
+
131
+ it("should not affect citizen IDs validation", () => {
132
+ const schemaWithOld = nationalId({ type: "citizen", allowOldResident: true })
133
+ const schemaWithoutOld = nationalId({ type: "citizen", allowOldResident: false })
134
+
135
+ // Both should accept citizen IDs
136
+ expect(schemaWithOld.parse("A123456789")).toBe("A123456789")
137
+ expect(schemaWithoutOld.parse("A123456789")).toBe("A123456789")
138
+
139
+ // Both should reject resident IDs (since type is citizen)
140
+ expect(() => schemaWithOld.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
141
+ expect(() => schemaWithoutOld.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
142
+ expect(() => schemaWithOld.parse("A800000005")).toThrow("Invalid Taiwan National ID")
143
+ expect(() => schemaWithoutOld.parse("A800000005")).toThrow("Invalid Taiwan National ID")
144
+ })
145
+ })
146
+
147
+ describe("utility functions", () => {
148
+ describe("validateCitizenId", () => {
149
+ it("should correctly validate citizen IDs", () => {
150
+ // Valid citizen IDs
151
+ expect(validateCitizenId("A123456789")).toBe(true)
152
+ expect(validateCitizenId("B123456780")).toBe(true)
153
+ expect(validateCitizenId("Z187654324")).toBe(true)
154
+
155
+ // Invalid citizen IDs
156
+ expect(validateCitizenId("A123456788")).toBe(false) // Wrong checksum
157
+ expect(validateCitizenId("A323456789")).toBe(false) // Invalid gender digit
158
+ expect(validateCitizenId("AA00000001")).toBe(false) // Wrong format
159
+ expect(validateCitizenId("A12345678")).toBe(false) // Too short
160
+ })
161
+ })
162
+
163
+ describe("validateOldResidentId", () => {
164
+ it("should correctly validate old resident IDs", () => {
165
+ // Valid old resident IDs
166
+ expect(validateOldResidentId("AA00000001")).toBe(true)
167
+ expect(validateOldResidentId("AC12345677")).toBe(true)
168
+ expect(validateOldResidentId("ZB12345676")).toBe(true)
169
+ expect(validateOldResidentId("AD12345675")).toBe(true)
170
+
171
+ // Invalid old resident IDs
172
+ expect(validateOldResidentId("AE12345678")).toBe(false) // Invalid gender code
173
+ expect(validateOldResidentId("A123456789")).toBe(false) // Wrong format
174
+ expect(validateOldResidentId("AA1234567")).toBe(false) // Too short
175
+ })
176
+ })
177
+
178
+ describe("validateNewResidentId", () => {
179
+ it("should correctly validate new resident IDs", () => {
180
+ // Valid new resident IDs
181
+ expect(validateNewResidentId("A800000005")).toBe(true)
182
+ expect(validateNewResidentId("A912345673")).toBe(true)
183
+ expect(validateNewResidentId("Z812345672")).toBe(true)
184
+ expect(validateNewResidentId("Z912345674")).toBe(true)
185
+
186
+ // Invalid new resident IDs
187
+ expect(validateNewResidentId("A712345678")).toBe(false) // Invalid gender digit
188
+ expect(validateNewResidentId("A123456789")).toBe(false) // Wrong format
189
+ expect(validateNewResidentId("A81234567")).toBe(false) // Too short
190
+ })
191
+ })
192
+
193
+ describe("validateTaiwanNationalId", () => {
194
+ it("should validate with type parameter", () => {
195
+ // Test with citizen type
196
+ expect(validateTaiwanNationalId("A123456789", "citizen")).toBe(true)
197
+ expect(validateTaiwanNationalId("AA00000001", "citizen")).toBe(false)
198
+
199
+ // Test with resident type
200
+ expect(validateTaiwanNationalId("AA00000001", "resident")).toBe(true)
201
+ expect(validateTaiwanNationalId("A800000005", "resident")).toBe(true)
202
+ expect(validateTaiwanNationalId("A123456789", "resident")).toBe(false)
203
+
204
+ // Test with both type
205
+ expect(validateTaiwanNationalId("A123456789", "both")).toBe(true)
206
+ expect(validateTaiwanNationalId("AA00000001", "both")).toBe(true)
207
+ expect(validateTaiwanNationalId("A800000005", "both")).toBe(true)
208
+ })
209
+ })
210
+ })
211
+
212
+ describe("required/optional behavior", () => {
213
+ it("should handle required=true (default)", () => {
214
+ const schema = nationalId()
215
+
216
+ expect(() => schema.parse("")).toThrow("Required")
217
+ expect(() => schema.parse(null)).toThrow()
218
+ expect(() => schema.parse(undefined)).toThrow()
219
+ })
220
+
221
+ it("should handle required=false", () => {
222
+ const schema = nationalId({ required: false })
223
+
224
+ expect(schema.parse("")).toBe(null)
225
+ expect(schema.parse(null)).toBe(null)
226
+ expect(schema.parse(undefined)).toBe(null)
227
+ expect(schema.parse("A123456789")).toBe("A123456789")
228
+ })
229
+
230
+ it("should use default values", () => {
231
+ const requiredSchema = nationalId({ defaultValue: "A123456789" })
232
+ const optionalSchema = nationalId({ required: false, defaultValue: "A123456789" })
233
+
234
+ expect(requiredSchema.parse("")).toBe("A123456789")
235
+ expect(optionalSchema.parse("")).toBe("A123456789")
236
+ })
237
+ })
238
+
239
+ describe("transform function", () => {
240
+ it("should apply custom transform", () => {
241
+ const schema = nationalId({
242
+ transform: (val) => val.replace(/[-\s]/g, "").toUpperCase(),
243
+ })
244
+
245
+ expect(schema.parse("a12-345-6789")).toBe("A123456789")
246
+ expect(schema.parse("z18 765 4324")).toBe("Z187654324")
247
+ })
248
+
249
+ it("should apply transform before validation", () => {
250
+ const schema = nationalId({
251
+ transform: (val) => val.replace(/\s+/g, "").toUpperCase(),
252
+ })
253
+
254
+ expect(schema.parse(" a123456789 ")).toBe("A123456789")
255
+ expect(() => schema.parse(" a123456788 ")).toThrow("Invalid Taiwan National ID")
256
+ })
257
+ })
258
+
259
+ describe("input preprocessing", () => {
260
+ it("should handle string conversion and case normalization", () => {
261
+ const schema = nationalId()
262
+
263
+ // Test automatic uppercase conversion
264
+ expect(schema.parse("a123456789")).toBe("A123456789")
265
+ expect(schema.parse("z187654324")).toBe("Z187654324")
266
+ })
267
+
268
+ it("should trim whitespace", () => {
269
+ const schema = nationalId()
270
+
271
+ expect(schema.parse(" A123456789 ")).toBe("A123456789")
272
+ expect(schema.parse("\tZ187654324\n")).toBe("Z187654324")
273
+ })
274
+ })
275
+
276
+ describe("i18n support", () => {
277
+ it("should use English messages by default", () => {
278
+ setLocale("en")
279
+ const schema = nationalId()
280
+
281
+ expect(() => schema.parse("")).toThrow("Required")
282
+ expect(() => schema.parse("A123456788")).toThrow("Invalid Taiwan National ID")
283
+ })
284
+
285
+ it("should use Chinese messages when locale is zh-TW", () => {
286
+ setLocale("zh-TW")
287
+ const schema = nationalId()
288
+
289
+ expect(() => schema.parse("")).toThrow("必填")
290
+ expect(() => schema.parse("A123456788")).toThrow("無效的身分證字號")
291
+ })
292
+
293
+ it("should support custom i18n messages", () => {
294
+ const schema = nationalId({
295
+ i18n: {
296
+ en: {
297
+ required: "National ID is required",
298
+ invalid: "National ID is invalid",
299
+ },
300
+ "zh-TW": {
301
+ required: "請輸入身分證字號",
302
+ invalid: "身分證字號格式錯誤",
303
+ },
304
+ },
305
+ })
306
+
307
+ setLocale("en")
308
+ expect(() => schema.parse("")).toThrow("National ID is required")
309
+ expect(() => schema.parse("A123456788")).toThrow("National ID is invalid")
310
+
311
+ setLocale("zh-TW")
312
+ expect(() => schema.parse("")).toThrow("請輸入身分證字號")
313
+ expect(() => schema.parse("A123456788")).toThrow("身分證字號格式錯誤")
314
+ })
315
+ })
316
+
317
+ describe("real world Taiwan national IDs", () => {
318
+ it("should validate known citizen ID patterns", () => {
319
+ const schema = nationalId({ type: "citizen" })
320
+
321
+ // Test various city codes and gender combinations
322
+ const validCitizenIds = [
323
+ "A123456789", // Taipei male
324
+ "A223456781", // Taipei female
325
+ "B123456780", // Taichung male
326
+ "B223456782", // Taichung female
327
+ "Z187654324", // Lianjiang male
328
+ "Z287654326", // Lianjiang female
329
+ ]
330
+
331
+ validCitizenIds.forEach((id) => {
332
+ expect(schema.parse(id)).toBe(id)
333
+ })
334
+ })
335
+
336
+ it("should validate known old resident ID patterns", () => {
337
+ const schema = nationalId({ type: "resident" })
338
+
339
+ // Test old format resident IDs
340
+ const validOldResidentIds = [
341
+ "AA00000001", // Male
342
+ "AB00000009", // Female
343
+ "AC12345677", // Male
344
+ "AD12345675", // Female
345
+ "ZA12345678", // Male
346
+ "ZB12345676", // Female
347
+ ]
348
+
349
+ validOldResidentIds.forEach((id) => {
350
+ expect(schema.parse(id)).toBe(id)
351
+ })
352
+ })
353
+
354
+ it("should validate known new resident ID patterns", () => {
355
+ const schema = nationalId({ type: "resident" })
356
+
357
+ // Test new format resident IDs
358
+ const validNewResidentIds = [
359
+ "A800000005", // Male
360
+ "A912345673", // Female
361
+ "B800000006", // Male
362
+ "B912345674", // Female
363
+ "Z812345672", // Male
364
+ "Z912345674", // Female
365
+ ]
366
+
367
+ validNewResidentIds.forEach((id) => {
368
+ expect(schema.parse(id)).toBe(id)
369
+ })
370
+ })
371
+
372
+ it("should reject common invalid patterns", () => {
373
+ const schema = nationalId()
374
+
375
+ const invalidIds = [
376
+ "A000000000", // All zeros
377
+ "A111111111", // All ones
378
+ "A123456788", // Wrong checksum
379
+ "A323456789", // Invalid gender digit for citizen
380
+ "A712345678", // Invalid gender digit for new resident
381
+ "AE12345678", // Invalid gender code for old resident
382
+ "1123456789", // No letter
383
+ "A023456789", // Invalid gender digit
384
+ "A12345678", // Too short
385
+ "A1234567890", // Too long
386
+ ]
387
+
388
+ invalidIds.forEach((id) => {
389
+ expect(() => schema.parse(id)).toThrow()
390
+ })
391
+ })
392
+ })
393
+
394
+ describe("edge cases", () => {
395
+ it("should handle all valid city codes", () => {
396
+ const schema = nationalId({ type: "citizen" })
397
+
398
+ // Test all valid city codes
399
+ const cityCodes = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
400
+
401
+ cityCodes.forEach((code) => {
402
+ // Test with a known valid pattern (need to calculate actual valid IDs)
403
+ const testId = code + "123456789"
404
+ try {
405
+ schema.parse(testId)
406
+ // If no error thrown, the ID is valid
407
+ } catch (error: any) {
408
+ // If error thrown, it's expected for invalid checksums
409
+ expect(error.message).toContain("Invalid Taiwan National ID")
410
+ }
411
+ })
412
+ })
413
+
414
+ it("should handle empty and whitespace inputs", () => {
415
+ const schema = nationalId()
416
+ const optionalSchema = nationalId({ required: false })
417
+
418
+ expect(() => schema.parse("")).toThrow("Required")
419
+ expect(() => schema.parse(" ")).toThrow("Required")
420
+ expect(() => schema.parse("\t\n")).toThrow("Required")
421
+
422
+ expect(optionalSchema.parse("")).toBe(null)
423
+ expect(optionalSchema.parse(" ")).toBe(null)
424
+ expect(optionalSchema.parse("\t\n")).toBe(null)
425
+ })
426
+
427
+ it("should preserve valid format after transformation", () => {
428
+ const schema = nationalId({
429
+ transform: (val) => val.replace(/[^A-Z0-9]/g, "").toUpperCase(),
430
+ })
431
+
432
+ expect(schema.parse("a-1@2#3$4%5^6&7*8(9)")).toBe("A123456789")
433
+ })
434
+ })
435
+ })