@hy_ong/zod-kit 0.2.0 → 0.2.2

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 (191) hide show
  1. package/.github/workflows/ci.yml +24 -0
  2. package/CLAUDE.md +64 -22
  3. package/dist/chunk-2SWEVDFZ.js +134 -0
  4. package/dist/chunk-32JI34CV.cjs +146 -0
  5. package/dist/chunk-42C5OHRK.js +71 -0
  6. package/dist/chunk-46VAH2BJ.js +160 -0
  7. package/dist/chunk-5JGTDL3Y.js +87 -0
  8. package/dist/chunk-5LEXCVLX.js +257 -0
  9. package/dist/chunk-6AAP4LPF.js +2606 -0
  10. package/dist/chunk-B4EZYZOK.cjs +215 -0
  11. package/dist/chunk-COYKBWTI.js +161 -0
  12. package/dist/chunk-DFJZ3NS2.cjs +151 -0
  13. package/dist/chunk-EDHT4LPO.js +118 -0
  14. package/dist/chunk-EGHL277K.cjs +165 -0
  15. package/dist/chunk-ERH4NIMU.cjs +69 -0
  16. package/dist/chunk-FM3EZ72O.js +165 -0
  17. package/dist/chunk-GJIRDBZJ.cjs +90 -0
  18. package/dist/chunk-H2XTEM4M.js +696 -0
  19. package/dist/chunk-HMSM6FFA.cjs +181 -0
  20. package/dist/chunk-HTEHINI7.cjs +177 -0
  21. package/dist/chunk-JOLSGZGN.cjs +696 -0
  22. package/dist/chunk-JXY7APBU.js +69 -0
  23. package/dist/chunk-K2UOY6TB.js +136 -0
  24. package/dist/chunk-KFOHKTFD.js +61 -0
  25. package/dist/chunk-L4HSIKTU.cjs +135 -0
  26. package/dist/chunk-LH7ZB4BK.js +124 -0
  27. package/dist/chunk-LL4ZWLGO.js +90 -0
  28. package/dist/chunk-M6MTP3NY.cjs +99 -0
  29. package/dist/chunk-MHJFYYGV.js +215 -0
  30. package/dist/chunk-MINMXGW3.js +135 -0
  31. package/dist/chunk-MM7IL2RG.js +181 -0
  32. package/dist/chunk-OPQJWHXN.cjs +301 -0
  33. package/dist/chunk-ORFHDJII.cjs +136 -0
  34. package/dist/chunk-ORVV4MCF.cjs +87 -0
  35. package/dist/chunk-QICQ6YEY.js +75 -0
  36. package/dist/chunk-RKUQREMW.js +127 -0
  37. package/dist/chunk-RO47DKQG.js +146 -0
  38. package/dist/chunk-RRPXIRTQ.cjs +257 -0
  39. package/dist/chunk-RYFG2GKM.cjs +118 -0
  40. package/dist/chunk-STNHTRG7.cjs +124 -0
  41. package/dist/chunk-TFGS34VD.cjs +71 -0
  42. package/dist/chunk-TQXDUMML.cjs +61 -0
  43. package/dist/chunk-UBK3VCVH.cjs +134 -0
  44. package/dist/chunk-UCOXAZJF.cjs +2606 -0
  45. package/dist/chunk-UQZKFAFX.js +130 -0
  46. package/dist/chunk-VB2KV2ZM.cjs +130 -0
  47. package/dist/chunk-WABKPFPK.js +151 -0
  48. package/dist/chunk-WDI4QJMQ.js +177 -0
  49. package/dist/chunk-YDH3L27K.cjs +127 -0
  50. package/dist/chunk-YIM3D2AD.js +99 -0
  51. package/dist/chunk-YPSEIDUR.cjs +160 -0
  52. package/dist/chunk-ZNJLWJX3.cjs +75 -0
  53. package/dist/chunk-ZTFCJCPO.cjs +161 -0
  54. package/dist/chunk-ZXUMK2RR.js +301 -0
  55. package/dist/common/boolean.cjs +7 -0
  56. package/dist/common/boolean.d.cts +119 -0
  57. package/dist/common/boolean.d.ts +119 -0
  58. package/dist/common/boolean.js +7 -0
  59. package/dist/common/color.cjs +9 -0
  60. package/dist/common/color.d.cts +26 -0
  61. package/dist/common/color.d.ts +26 -0
  62. package/dist/common/color.js +9 -0
  63. package/dist/common/coordinate.cjs +11 -0
  64. package/dist/common/coordinate.d.cts +23 -0
  65. package/dist/common/coordinate.d.ts +23 -0
  66. package/dist/common/coordinate.js +11 -0
  67. package/dist/common/credit-card.cjs +11 -0
  68. package/dist/common/credit-card.d.cts +22 -0
  69. package/dist/common/credit-card.d.ts +22 -0
  70. package/dist/common/credit-card.js +11 -0
  71. package/dist/common/date.cjs +7 -0
  72. package/dist/common/date.d.cts +174 -0
  73. package/dist/common/date.d.ts +174 -0
  74. package/dist/common/date.js +7 -0
  75. package/dist/common/datetime.cjs +15 -0
  76. package/dist/common/datetime.d.cts +301 -0
  77. package/dist/common/datetime.d.ts +301 -0
  78. package/dist/common/datetime.js +15 -0
  79. package/dist/common/email.cjs +7 -0
  80. package/dist/common/email.d.cts +149 -0
  81. package/dist/common/email.d.ts +149 -0
  82. package/dist/common/email.js +7 -0
  83. package/dist/common/file.cjs +7 -0
  84. package/dist/common/file.d.cts +178 -0
  85. package/dist/common/file.d.ts +178 -0
  86. package/dist/common/file.js +7 -0
  87. package/dist/common/id.cjs +13 -0
  88. package/dist/common/id.d.cts +288 -0
  89. package/dist/common/id.d.ts +288 -0
  90. package/dist/common/id.js +13 -0
  91. package/dist/common/ip.cjs +11 -0
  92. package/dist/common/ip.d.cts +25 -0
  93. package/dist/common/ip.d.ts +25 -0
  94. package/dist/common/ip.js +11 -0
  95. package/dist/common/number.cjs +7 -0
  96. package/dist/common/number.d.cts +167 -0
  97. package/dist/common/number.d.ts +167 -0
  98. package/dist/common/number.js +7 -0
  99. package/dist/common/password.cjs +7 -0
  100. package/dist/common/password.d.cts +192 -0
  101. package/dist/common/password.d.ts +192 -0
  102. package/dist/common/password.js +7 -0
  103. package/dist/common/text.cjs +7 -0
  104. package/dist/common/text.d.cts +156 -0
  105. package/dist/common/text.d.ts +156 -0
  106. package/dist/common/text.js +7 -0
  107. package/dist/common/time.cjs +15 -0
  108. package/dist/common/time.d.cts +268 -0
  109. package/dist/common/time.d.ts +268 -0
  110. package/dist/common/time.js +15 -0
  111. package/dist/common/url.cjs +7 -0
  112. package/dist/common/url.d.cts +196 -0
  113. package/dist/common/url.d.ts +196 -0
  114. package/dist/common/url.js +7 -0
  115. package/dist/config-CABSSvAp.d.cts +5 -0
  116. package/dist/config-CABSSvAp.d.ts +5 -0
  117. package/dist/index.cjs +180 -5255
  118. package/dist/index.d.cts +28 -3150
  119. package/dist/index.d.ts +28 -3150
  120. package/dist/index.js +135 -5131
  121. package/dist/taiwan/bank-account.cjs +11 -0
  122. package/dist/taiwan/bank-account.d.cts +22 -0
  123. package/dist/taiwan/bank-account.d.ts +22 -0
  124. package/dist/taiwan/bank-account.js +11 -0
  125. package/dist/taiwan/business-id.cjs +9 -0
  126. package/dist/taiwan/business-id.d.cts +133 -0
  127. package/dist/taiwan/business-id.d.ts +133 -0
  128. package/dist/taiwan/business-id.js +9 -0
  129. package/dist/taiwan/fax.cjs +9 -0
  130. package/dist/taiwan/fax.d.cts +157 -0
  131. package/dist/taiwan/fax.d.ts +157 -0
  132. package/dist/taiwan/fax.js +9 -0
  133. package/dist/taiwan/invoice.cjs +9 -0
  134. package/dist/taiwan/invoice.d.cts +17 -0
  135. package/dist/taiwan/invoice.d.ts +17 -0
  136. package/dist/taiwan/invoice.js +9 -0
  137. package/dist/taiwan/license-plate.cjs +9 -0
  138. package/dist/taiwan/license-plate.d.cts +19 -0
  139. package/dist/taiwan/license-plate.d.ts +19 -0
  140. package/dist/taiwan/license-plate.js +9 -0
  141. package/dist/taiwan/mobile.cjs +9 -0
  142. package/dist/taiwan/mobile.d.cts +146 -0
  143. package/dist/taiwan/mobile.d.ts +146 -0
  144. package/dist/taiwan/mobile.js +9 -0
  145. package/dist/taiwan/national-id.cjs +15 -0
  146. package/dist/taiwan/national-id.d.cts +214 -0
  147. package/dist/taiwan/national-id.d.ts +214 -0
  148. package/dist/taiwan/national-id.js +15 -0
  149. package/dist/taiwan/passport.cjs +9 -0
  150. package/dist/taiwan/passport.d.cts +19 -0
  151. package/dist/taiwan/passport.d.ts +19 -0
  152. package/dist/taiwan/passport.js +9 -0
  153. package/dist/taiwan/postal-code.cjs +17 -0
  154. package/dist/taiwan/postal-code.d.cts +237 -0
  155. package/dist/taiwan/postal-code.d.ts +237 -0
  156. package/dist/taiwan/postal-code.js +17 -0
  157. package/dist/taiwan/tel.cjs +9 -0
  158. package/dist/taiwan/tel.d.cts +162 -0
  159. package/dist/taiwan/tel.d.ts +162 -0
  160. package/dist/taiwan/tel.js +9 -0
  161. package/package.json +132 -6
  162. package/src/i18n/locales/en-GB.json +51 -0
  163. package/src/i18n/locales/en-US.json +52 -1
  164. package/src/i18n/locales/id-ID.json +51 -0
  165. package/src/i18n/locales/ja-JP.json +51 -0
  166. package/src/i18n/locales/ko-KR.json +51 -0
  167. package/src/i18n/locales/ms-MY.json +51 -0
  168. package/src/i18n/locales/th-TH.json +51 -0
  169. package/src/i18n/locales/vi-VN.json +51 -0
  170. package/src/i18n/locales/zh-CN.json +51 -0
  171. package/src/i18n/locales/zh-TW.json +51 -0
  172. package/src/index.ts +10 -2
  173. package/src/validators/common/color.ts +192 -0
  174. package/src/validators/common/coordinate.ts +159 -0
  175. package/src/validators/common/credit-card.ts +134 -0
  176. package/src/validators/common/id.ts +45 -3
  177. package/src/validators/common/ip.ts +210 -0
  178. package/src/validators/taiwan/bank-account.ts +176 -0
  179. package/src/validators/taiwan/invoice.ts +84 -0
  180. package/src/validators/taiwan/license-plate.ts +110 -0
  181. package/src/validators/taiwan/passport.ts +103 -0
  182. package/tests/common/color.test.ts +587 -0
  183. package/tests/common/coordinate.test.ts +345 -0
  184. package/tests/common/credit-card.test.ts +378 -0
  185. package/tests/common/id.test.ts +68 -3
  186. package/tests/common/ip.test.ts +419 -0
  187. package/tests/taiwan/bank-account.test.ts +286 -0
  188. package/tests/taiwan/invoice.test.ts +227 -0
  189. package/tests/taiwan/license-plate.test.ts +280 -0
  190. package/tests/taiwan/passport.test.ts +277 -0
  191. package/tsup.config.ts +36 -0
@@ -0,0 +1,280 @@
1
+ import { describe, it, expect, beforeEach } from "vitest"
2
+ import { twLicensePlate, setLocale, validateTaiwanLicensePlate } from "../../src"
3
+
4
+ describe("Taiwan twLicensePlate(true) validator", () => {
5
+ beforeEach(() => setLocale("en-US"))
6
+
7
+ describe("basic functionality", () => {
8
+ it("should validate new-style car plates (3 letters + 4 digits)", () => {
9
+ const schema = twLicensePlate(true)
10
+
11
+ expect(schema.parse("ABC1234")).toBe("ABC1234")
12
+ expect(schema.parse("XYZ9876")).toBe("XYZ9876")
13
+ })
14
+
15
+ it("should validate new-style car plates with hyphen stripped (ABC-1234)", () => {
16
+ const schema = twLicensePlate(true)
17
+
18
+ expect(schema.parse("ABC-1234")).toBe("ABC1234")
19
+ })
20
+
21
+ it("should validate car plates with 4 digits + 2 letters", () => {
22
+ const schema = twLicensePlate(true)
23
+
24
+ expect(schema.parse("1234AB")).toBe("1234AB")
25
+ expect(schema.parse("1234-AB")).toBe("1234AB")
26
+ })
27
+
28
+ it("should validate legacy car plates (2 letters + 4 digits)", () => {
29
+ const schema = twLicensePlate(true)
30
+
31
+ expect(schema.parse("AB1234")).toBe("AB1234")
32
+ expect(schema.parse("AB-1234")).toBe("AB1234")
33
+ })
34
+
35
+ it("should validate legacy mixed car plates (1 letter + 5 digits)", () => {
36
+ const schema = twLicensePlate(true)
37
+
38
+ expect(schema.parse("A12345")).toBe("A12345")
39
+ })
40
+
41
+ it("should validate motorcycle plates (3 digits + 3 letters)", () => {
42
+ const schema = twLicensePlate(true)
43
+
44
+ expect(schema.parse("123ABC")).toBe("123ABC")
45
+ expect(schema.parse("123-ABC")).toBe("123ABC")
46
+ })
47
+
48
+ it("should auto-uppercase lowercase input", () => {
49
+ const schema = twLicensePlate(true)
50
+
51
+ expect(schema.parse("abc1234")).toBe("ABC1234")
52
+ expect(schema.parse("abc-1234")).toBe("ABC1234")
53
+ })
54
+
55
+ it("should reject plates that are too short", () => {
56
+ const schema = twLicensePlate(true)
57
+
58
+ expect(() => schema.parse("AB123")).toThrow("Invalid Taiwan license plate number")
59
+ })
60
+
61
+ it("should reject just digits", () => {
62
+ const schema = twLicensePlate(true)
63
+
64
+ expect(() => schema.parse("1234")).toThrow("Invalid Taiwan license plate number")
65
+ expect(() => schema.parse("1234567")).toThrow("Invalid Taiwan license plate number")
66
+ })
67
+
68
+ it("should reject 4 letters + 4 digits", () => {
69
+ const schema = twLicensePlate(true)
70
+
71
+ expect(() => schema.parse("ABCD1234")).toThrow("Invalid Taiwan license plate number")
72
+ })
73
+ })
74
+
75
+ describe("plateType option", () => {
76
+ it("should validate car plates only when plateType is 'car'", () => {
77
+ const schema = twLicensePlate(true, { plateType: "car" })
78
+
79
+ expect(schema.parse("ABC1234")).toBe("ABC1234")
80
+ expect(schema.parse("1234AB")).toBe("1234AB")
81
+ expect(schema.parse("AB1234")).toBe("AB1234")
82
+ expect(schema.parse("A12345")).toBe("A12345")
83
+ })
84
+
85
+ it("should reject motorcycle-only plates when plateType is 'car'", () => {
86
+ const schema = twLicensePlate(true, { plateType: "car" })
87
+
88
+ // 123ABC (3 digits + 3 letters) is motorcycle-only
89
+ expect(() => schema.parse("123ABC")).toThrow("Invalid Taiwan license plate number")
90
+ })
91
+
92
+ it("should validate motorcycle plates when plateType is 'motorcycle'", () => {
93
+ const schema = twLicensePlate(true, { plateType: "motorcycle" })
94
+
95
+ expect(schema.parse("ABC1234")).toBe("ABC1234")
96
+ expect(schema.parse("123ABC")).toBe("123ABC")
97
+ expect(schema.parse("AB1234")).toBe("AB1234")
98
+ expect(schema.parse("1234AB")).toBe("1234AB")
99
+ })
100
+
101
+ it("should reject car-only plates when plateType is 'motorcycle'", () => {
102
+ const schema = twLicensePlate(true, { plateType: "motorcycle" })
103
+
104
+ // A12345 (1 letter + 5 digits) is car-only
105
+ expect(() => schema.parse("A12345")).toThrow("Invalid Taiwan license plate number")
106
+ })
107
+
108
+ it("should validate all plate types when plateType is 'any'", () => {
109
+ const schema = twLicensePlate(true, { plateType: "any" })
110
+
111
+ expect(schema.parse("ABC1234")).toBe("ABC1234")
112
+ expect(schema.parse("1234AB")).toBe("1234AB")
113
+ expect(schema.parse("AB1234")).toBe("AB1234")
114
+ expect(schema.parse("A12345")).toBe("A12345")
115
+ expect(schema.parse("123ABC")).toBe("123ABC")
116
+ })
117
+ })
118
+
119
+ describe("utility function validateTaiwanLicensePlate", () => {
120
+ it("should return true for valid plate numbers", () => {
121
+ expect(validateTaiwanLicensePlate("ABC1234")).toBe(true)
122
+ expect(validateTaiwanLicensePlate("1234AB")).toBe(true)
123
+ expect(validateTaiwanLicensePlate("AB1234")).toBe(true)
124
+ expect(validateTaiwanLicensePlate("A12345")).toBe(true)
125
+ expect(validateTaiwanLicensePlate("123ABC")).toBe(true)
126
+ })
127
+
128
+ it("should return false for invalid plate numbers", () => {
129
+ expect(validateTaiwanLicensePlate("AB123")).toBe(false)
130
+ expect(validateTaiwanLicensePlate("ABCD1234")).toBe(false)
131
+ expect(validateTaiwanLicensePlate("1234")).toBe(false)
132
+ expect(validateTaiwanLicensePlate("")).toBe(false)
133
+ expect(validateTaiwanLicensePlate("abc1234")).toBe(false) // lowercase
134
+ })
135
+
136
+ it("should respect plateType parameter", () => {
137
+ // A12345 is car-only
138
+ expect(validateTaiwanLicensePlate("A12345", "car")).toBe(true)
139
+ expect(validateTaiwanLicensePlate("A12345", "motorcycle")).toBe(false)
140
+ expect(validateTaiwanLicensePlate("A12345", "any")).toBe(true)
141
+
142
+ // 123ABC is motorcycle-only
143
+ expect(validateTaiwanLicensePlate("123ABC", "motorcycle")).toBe(true)
144
+ expect(validateTaiwanLicensePlate("123ABC", "car")).toBe(false)
145
+ expect(validateTaiwanLicensePlate("123ABC", "any")).toBe(true)
146
+ })
147
+ })
148
+
149
+ describe("required/optional behavior", () => {
150
+ it("should handle required=true", () => {
151
+ const schema = twLicensePlate(true)
152
+
153
+ expect(() => schema.parse("")).toThrow("Required")
154
+ expect(() => schema.parse(null)).toThrow()
155
+ expect(() => schema.parse(undefined)).toThrow()
156
+ })
157
+
158
+ it("should handle required=false", () => {
159
+ const schema = twLicensePlate(false)
160
+
161
+ expect(schema.parse("")).toBe(null)
162
+ expect(schema.parse(null)).toBe(null)
163
+ expect(schema.parse(undefined)).toBe(null)
164
+ expect(schema.parse("ABC1234")).toBe("ABC1234")
165
+ })
166
+
167
+ it("should use default values", () => {
168
+ const requiredSchema = twLicensePlate(true, { defaultValue: "ABC1234" })
169
+ const optionalSchema = twLicensePlate(false, { defaultValue: "ABC1234" })
170
+
171
+ expect(requiredSchema.parse("")).toBe("ABC1234")
172
+ expect(optionalSchema.parse("")).toBe("ABC1234")
173
+ })
174
+ })
175
+
176
+ describe("transform function", () => {
177
+ it("should apply custom transform", () => {
178
+ const schema = twLicensePlate(true, {
179
+ transform: (val) => val.replace(/\s+/g, ""),
180
+ })
181
+
182
+ expect(schema.parse("ABC 1234")).toBe("ABC1234")
183
+ })
184
+
185
+ it("should apply transform before validation", () => {
186
+ const schema = twLicensePlate(true, {
187
+ transform: (val) => val.replace(/\s+/g, ""),
188
+ })
189
+
190
+ expect(schema.parse(" ABC1234 ")).toBe("ABC1234")
191
+ })
192
+ })
193
+
194
+ describe("input preprocessing", () => {
195
+ it("should trim whitespace", () => {
196
+ const schema = twLicensePlate(true)
197
+
198
+ expect(schema.parse(" ABC1234 ")).toBe("ABC1234")
199
+ expect(schema.parse("\tABC1234\n")).toBe("ABC1234")
200
+ })
201
+
202
+ it("should strip hyphens and spaces from input", () => {
203
+ const schema = twLicensePlate(true)
204
+
205
+ expect(schema.parse("ABC-1234")).toBe("ABC1234")
206
+ expect(schema.parse("123-ABC")).toBe("123ABC")
207
+ expect(schema.parse("AB-1234")).toBe("AB1234")
208
+ })
209
+ })
210
+
211
+ describe("i18n support", () => {
212
+ it("should use English messages by default", () => {
213
+ setLocale("en-US")
214
+ const schema = twLicensePlate(true)
215
+
216
+ expect(() => schema.parse("")).toThrow("Required")
217
+ expect(() => schema.parse("INVALID")).toThrow("Invalid Taiwan license plate number")
218
+ })
219
+
220
+ it("should use Chinese messages when locale is zh-TW", () => {
221
+ setLocale("zh-TW")
222
+ const schema = twLicensePlate(true)
223
+
224
+ expect(() => schema.parse("")).toThrow("必填")
225
+ expect(() => schema.parse("INVALID")).toThrow("無效的車牌號碼")
226
+ })
227
+
228
+ it("should support custom i18n messages", () => {
229
+ const schema = twLicensePlate(true, {
230
+ i18n: {
231
+ "en-US": {
232
+ required: "Plate number is required",
233
+ invalid: "Plate number is invalid",
234
+ },
235
+ "zh-TW": {
236
+ required: "請輸入車牌號碼",
237
+ invalid: "車牌號碼格式錯誤",
238
+ },
239
+ },
240
+ })
241
+
242
+ setLocale("en-US")
243
+ expect(() => schema.parse("")).toThrow("Plate number is required")
244
+ expect(() => schema.parse("INVALID")).toThrow("Plate number is invalid")
245
+
246
+ setLocale("zh-TW")
247
+ expect(() => schema.parse("")).toThrow("請輸入車牌號碼")
248
+ expect(() => schema.parse("INVALID")).toThrow("車牌號碼格式錯誤")
249
+ })
250
+ })
251
+
252
+ describe("edge cases", () => {
253
+ it("should handle empty and whitespace inputs", () => {
254
+ const schema = twLicensePlate(true)
255
+ const optionalSchema = twLicensePlate(false)
256
+
257
+ expect(() => schema.parse("")).toThrow("Required")
258
+ expect(() => schema.parse(" ")).toThrow("Required")
259
+ expect(() => schema.parse("\t\n")).toThrow("Required")
260
+
261
+ expect(optionalSchema.parse("")).toBe(null)
262
+ expect(optionalSchema.parse(" ")).toBe(null)
263
+ expect(optionalSchema.parse("\t\n")).toBe(null)
264
+ })
265
+
266
+ it("should reject just letters", () => {
267
+ const schema = twLicensePlate(true)
268
+
269
+ expect(() => schema.parse("ABCDEF")).toThrow("Invalid Taiwan license plate number")
270
+ expect(() => schema.parse("ABC")).toThrow("Invalid Taiwan license plate number")
271
+ })
272
+
273
+ it("should reject mixed patterns that do not match any format", () => {
274
+ const schema = twLicensePlate(true)
275
+
276
+ expect(() => schema.parse("A1B2C3")).toThrow("Invalid Taiwan license plate number")
277
+ expect(() => schema.parse("1A2B3C")).toThrow("Invalid Taiwan license plate number")
278
+ })
279
+ })
280
+ })
@@ -0,0 +1,277 @@
1
+ import { describe, it, expect, beforeEach } from "vitest"
2
+ import { twPassport, setLocale, validateTaiwanPassport } from "../../src"
3
+
4
+ describe("Taiwan twPassport(true) validator", () => {
5
+ beforeEach(() => setLocale("en-US"))
6
+
7
+ describe("basic functionality", () => {
8
+ it("should validate ordinary passport numbers (starts with 2)", () => {
9
+ const schema = twPassport(true)
10
+
11
+ expect(schema.parse("200000001")).toBe("200000001")
12
+ expect(schema.parse("299999999")).toBe("299999999")
13
+ })
14
+
15
+ it("should validate official passport numbers (starts with 1)", () => {
16
+ const schema = twPassport(true)
17
+
18
+ expect(schema.parse("100000001")).toBe("100000001")
19
+ expect(schema.parse("199999999")).toBe("199999999")
20
+ })
21
+
22
+ it("should validate diplomatic passport numbers (starts with 0)", () => {
23
+ const schema = twPassport(true)
24
+
25
+ expect(schema.parse("000000001")).toBe("000000001")
26
+ expect(schema.parse("099999999")).toBe("099999999")
27
+ })
28
+
29
+ it("should validate travel document numbers (starts with 3)", () => {
30
+ const schema = twPassport(true)
31
+
32
+ expect(schema.parse("300000001")).toBe("300000001")
33
+ expect(schema.parse("399999999")).toBe("399999999")
34
+ })
35
+
36
+ it("should reject passport numbers starting with 4 or higher", () => {
37
+ const schema = twPassport(true)
38
+
39
+ expect(() => schema.parse("400000001")).toThrow("Invalid Taiwan passport number")
40
+ expect(() => schema.parse("500000001")).toThrow("Invalid Taiwan passport number")
41
+ expect(() => schema.parse("900000001")).toThrow("Invalid Taiwan passport number")
42
+ })
43
+
44
+ it("should reject passport numbers with wrong length (8 digits)", () => {
45
+ const schema = twPassport(true)
46
+
47
+ expect(() => schema.parse("12345678")).toThrow("Invalid Taiwan passport number")
48
+ })
49
+
50
+ it("should reject passport numbers with wrong length (10 digits)", () => {
51
+ const schema = twPassport(true)
52
+
53
+ expect(() => schema.parse("1234567890")).toThrow("Invalid Taiwan passport number")
54
+ })
55
+
56
+ it("should reject non-numeric input", () => {
57
+ const schema = twPassport(true)
58
+
59
+ expect(() => schema.parse("abcdefghi")).toThrow("Invalid Taiwan passport number")
60
+ expect(() => schema.parse("12345678A")).toThrow("Invalid Taiwan passport number")
61
+ })
62
+ })
63
+
64
+ describe("passportType option", () => {
65
+ it("should accept only ordinary passports (type 2) when passportType is 'ordinary'", () => {
66
+ const schema = twPassport(true, { passportType: "ordinary" })
67
+
68
+ expect(schema.parse("200000001")).toBe("200000001")
69
+ expect(() => schema.parse("000000001")).toThrow("Invalid Taiwan passport number")
70
+ expect(() => schema.parse("100000001")).toThrow("Invalid Taiwan passport number")
71
+ expect(() => schema.parse("300000001")).toThrow("Invalid Taiwan passport number")
72
+ })
73
+
74
+ it("should accept only diplomatic passports (type 0) when passportType is 'diplomatic'", () => {
75
+ const schema = twPassport(true, { passportType: "diplomatic" })
76
+
77
+ expect(schema.parse("000000001")).toBe("000000001")
78
+ expect(() => schema.parse("100000001")).toThrow("Invalid Taiwan passport number")
79
+ expect(() => schema.parse("200000001")).toThrow("Invalid Taiwan passport number")
80
+ expect(() => schema.parse("300000001")).toThrow("Invalid Taiwan passport number")
81
+ })
82
+
83
+ it("should accept only official passports (type 1) when passportType is 'official'", () => {
84
+ const schema = twPassport(true, { passportType: "official" })
85
+
86
+ expect(schema.parse("100000001")).toBe("100000001")
87
+ expect(() => schema.parse("000000001")).toThrow("Invalid Taiwan passport number")
88
+ expect(() => schema.parse("200000001")).toThrow("Invalid Taiwan passport number")
89
+ expect(() => schema.parse("300000001")).toThrow("Invalid Taiwan passport number")
90
+ })
91
+
92
+ it("should accept only travel documents (type 3) when passportType is 'travel'", () => {
93
+ const schema = twPassport(true, { passportType: "travel" })
94
+
95
+ expect(schema.parse("300000001")).toBe("300000001")
96
+ expect(() => schema.parse("000000001")).toThrow("Invalid Taiwan passport number")
97
+ expect(() => schema.parse("100000001")).toThrow("Invalid Taiwan passport number")
98
+ expect(() => schema.parse("200000001")).toThrow("Invalid Taiwan passport number")
99
+ })
100
+
101
+ it("should accept all valid passport types when passportType is 'any'", () => {
102
+ const schema = twPassport(true, { passportType: "any" })
103
+
104
+ expect(schema.parse("000000001")).toBe("000000001")
105
+ expect(schema.parse("100000001")).toBe("100000001")
106
+ expect(schema.parse("200000001")).toBe("200000001")
107
+ expect(schema.parse("300000001")).toBe("300000001")
108
+ })
109
+
110
+ it("should default to 'any' when passportType is not specified", () => {
111
+ const schema = twPassport(true)
112
+
113
+ expect(schema.parse("000000001")).toBe("000000001")
114
+ expect(schema.parse("100000001")).toBe("100000001")
115
+ expect(schema.parse("200000001")).toBe("200000001")
116
+ expect(schema.parse("300000001")).toBe("300000001")
117
+ })
118
+ })
119
+
120
+ describe("utility function validateTaiwanPassport", () => {
121
+ it("should return true for valid passport numbers", () => {
122
+ expect(validateTaiwanPassport("000000001")).toBe(true)
123
+ expect(validateTaiwanPassport("100000001")).toBe(true)
124
+ expect(validateTaiwanPassport("200000001")).toBe(true)
125
+ expect(validateTaiwanPassport("300000001")).toBe(true)
126
+ })
127
+
128
+ it("should return false for invalid passport numbers", () => {
129
+ expect(validateTaiwanPassport("400000001")).toBe(false)
130
+ expect(validateTaiwanPassport("12345678")).toBe(false)
131
+ expect(validateTaiwanPassport("1234567890")).toBe(false)
132
+ expect(validateTaiwanPassport("abcdefghi")).toBe(false)
133
+ expect(validateTaiwanPassport("")).toBe(false)
134
+ expect(validateTaiwanPassport("A00000001")).toBe(false)
135
+ })
136
+ })
137
+
138
+ describe("required/optional behavior", () => {
139
+ it("should handle required=true", () => {
140
+ const schema = twPassport(true)
141
+
142
+ expect(() => schema.parse("")).toThrow("Required")
143
+ expect(() => schema.parse(null)).toThrow()
144
+ expect(() => schema.parse(undefined)).toThrow()
145
+ })
146
+
147
+ it("should handle required=false", () => {
148
+ const schema = twPassport(false)
149
+
150
+ expect(schema.parse("")).toBe(null)
151
+ expect(schema.parse(null)).toBe(null)
152
+ expect(schema.parse(undefined)).toBe(null)
153
+ expect(schema.parse("200000001")).toBe("200000001")
154
+ })
155
+
156
+ it("should use default values", () => {
157
+ const requiredSchema = twPassport(true, { defaultValue: "200000001" })
158
+ const optionalSchema = twPassport(false, { defaultValue: "200000001" })
159
+
160
+ expect(requiredSchema.parse("")).toBe("200000001")
161
+ expect(optionalSchema.parse("")).toBe("200000001")
162
+ })
163
+ })
164
+
165
+ describe("transform function", () => {
166
+ it("should apply custom transform", () => {
167
+ const schema = twPassport(true, {
168
+ transform: (val) => val.replace(/-/g, ""),
169
+ })
170
+
171
+ expect(schema.parse("200-000-001")).toBe("200000001")
172
+ })
173
+
174
+ it("should apply transform before validation", () => {
175
+ const schema = twPassport(true, {
176
+ transform: (val) => val.replace(/\s+/g, ""),
177
+ })
178
+
179
+ expect(schema.parse(" 200000001 ")).toBe("200000001")
180
+ expect(schema.parse("2000 0000 1")).toBe("200000001")
181
+ // Transform cannot fix non-digit characters
182
+ expect(() => schema.parse("20000000X")).toThrow("Invalid Taiwan passport number")
183
+ })
184
+ })
185
+
186
+ describe("input preprocessing", () => {
187
+ it("should handle string conversion", () => {
188
+ const schema = twPassport(true)
189
+
190
+ expect(schema.parse(200000001)).toBe("200000001")
191
+ })
192
+
193
+ it("should trim whitespace", () => {
194
+ const schema = twPassport(true)
195
+
196
+ expect(schema.parse(" 200000001 ")).toBe("200000001")
197
+ expect(schema.parse("\t200000001\n")).toBe("200000001")
198
+ })
199
+ })
200
+
201
+ describe("i18n support", () => {
202
+ it("should use English messages by default", () => {
203
+ setLocale("en-US")
204
+ const schema = twPassport(true)
205
+
206
+ expect(() => schema.parse("")).toThrow("Required")
207
+ expect(() => schema.parse("INVALID")).toThrow("Invalid Taiwan passport number")
208
+ })
209
+
210
+ it("should use Chinese messages when locale is zh-TW", () => {
211
+ setLocale("zh-TW")
212
+ const schema = twPassport(true)
213
+
214
+ expect(() => schema.parse("")).toThrow("必填")
215
+ expect(() => schema.parse("INVALID")).toThrow("無效的護照號碼")
216
+ })
217
+
218
+ it("should support custom i18n messages", () => {
219
+ const schema = twPassport(true, {
220
+ i18n: {
221
+ "en-US": {
222
+ required: "Passport number is required",
223
+ invalid: "Passport number is invalid",
224
+ },
225
+ "zh-TW": {
226
+ required: "請輸入護照號碼",
227
+ invalid: "護照號碼格式錯誤",
228
+ },
229
+ },
230
+ })
231
+
232
+ setLocale("en-US")
233
+ expect(() => schema.parse("")).toThrow("Passport number is required")
234
+ expect(() => schema.parse("INVALID")).toThrow("Passport number is invalid")
235
+
236
+ setLocale("zh-TW")
237
+ expect(() => schema.parse("")).toThrow("請輸入護照號碼")
238
+ expect(() => schema.parse("INVALID")).toThrow("護照號碼格式錯誤")
239
+ })
240
+ })
241
+
242
+ describe("edge cases", () => {
243
+ it("should handle empty and whitespace inputs", () => {
244
+ const schema = twPassport(true)
245
+ const optionalSchema = twPassport(false)
246
+
247
+ expect(() => schema.parse("")).toThrow("Required")
248
+ expect(() => schema.parse(" ")).toThrow("Required")
249
+ expect(() => schema.parse("\t\n")).toThrow("Required")
250
+
251
+ expect(optionalSchema.parse("")).toBe(null)
252
+ expect(optionalSchema.parse(" ")).toBe(null)
253
+ expect(optionalSchema.parse("\t\n")).toBe(null)
254
+ })
255
+
256
+ it("should handle boundary values", () => {
257
+ const schema = twPassport(true)
258
+
259
+ expect(schema.parse("000000000")).toBe("000000000")
260
+ expect(schema.parse("399999999")).toBe("399999999")
261
+ })
262
+
263
+ it("should reject passport numbers with leading spaces that break digit count", () => {
264
+ const schema = twPassport(true)
265
+
266
+ // After trim, "2 00000001" becomes "2 00000001" which has a space
267
+ expect(() => schema.parse("2 00000001")).toThrow("Invalid Taiwan passport number")
268
+ })
269
+
270
+ it("should reject mixed alphanumeric input", () => {
271
+ const schema = twPassport(true)
272
+
273
+ expect(() => schema.parse("2A0000001")).toThrow("Invalid Taiwan passport number")
274
+ expect(() => schema.parse("20000000A")).toThrow("Invalid Taiwan passport number")
275
+ })
276
+ })
277
+ })
package/tsup.config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { defineConfig } from "tsup"
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: "src/index.ts",
6
+ "common/boolean": "src/validators/common/boolean.ts",
7
+ "common/date": "src/validators/common/date.ts",
8
+ "common/datetime": "src/validators/common/datetime.ts",
9
+ "common/email": "src/validators/common/email.ts",
10
+ "common/file": "src/validators/common/file.ts",
11
+ "common/id": "src/validators/common/id.ts",
12
+ "common/number": "src/validators/common/number.ts",
13
+ "common/password": "src/validators/common/password.ts",
14
+ "common/text": "src/validators/common/text.ts",
15
+ "common/time": "src/validators/common/time.ts",
16
+ "common/url": "src/validators/common/url.ts",
17
+ "common/credit-card": "src/validators/common/credit-card.ts",
18
+ "common/ip": "src/validators/common/ip.ts",
19
+ "common/color": "src/validators/common/color.ts",
20
+ "common/coordinate": "src/validators/common/coordinate.ts",
21
+ "taiwan/business-id": "src/validators/taiwan/business-id.ts",
22
+ "taiwan/national-id": "src/validators/taiwan/national-id.ts",
23
+ "taiwan/mobile": "src/validators/taiwan/mobile.ts",
24
+ "taiwan/postal-code": "src/validators/taiwan/postal-code.ts",
25
+ "taiwan/tel": "src/validators/taiwan/tel.ts",
26
+ "taiwan/fax": "src/validators/taiwan/fax.ts",
27
+ "taiwan/invoice": "src/validators/taiwan/invoice.ts",
28
+ "taiwan/license-plate": "src/validators/taiwan/license-plate.ts",
29
+ "taiwan/bank-account": "src/validators/taiwan/bank-account.ts",
30
+ "taiwan/passport": "src/validators/taiwan/passport.ts",
31
+ },
32
+ format: ["esm", "cjs"],
33
+ dts: true,
34
+ clean: true,
35
+ splitting: true,
36
+ })