@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.
- package/.claude/settings.local.json +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- 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
package/src/common/boolean.ts
DELETED
|
@@ -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
|
-
}
|
package/src/common/date.ts
DELETED
|
@@ -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
|
-
}
|
package/src/common/email.ts
DELETED
|
@@ -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
|
-
}
|
package/src/common/integer.ts
DELETED
|
@@ -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
|
-
}
|