@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,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Taiwan Landline Telephone Number validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides validation for Taiwan landline telephone numbers according to the
|
|
5
|
+
* official 2024 telecom numbering plan with comprehensive area code support.
|
|
6
|
+
*
|
|
7
|
+
* @author Ong Hoe Yuan
|
|
8
|
+
* @version 0.0.5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
12
|
+
import { t } from "../../i18n"
|
|
13
|
+
import { getLocale, type Locale } from "../../config"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type definition for telephone validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface TelMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [invalid] - Message when telephone number format is invalid
|
|
21
|
+
* @property {string} [notInWhitelist] - Message when telephone number is not in whitelist
|
|
22
|
+
*/
|
|
23
|
+
export type TelMessages = {
|
|
24
|
+
required?: string
|
|
25
|
+
invalid?: string
|
|
26
|
+
notInWhitelist?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration options for Taiwan landline telephone validation
|
|
31
|
+
*
|
|
32
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
33
|
+
*
|
|
34
|
+
* @interface TelOptions
|
|
35
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
36
|
+
* @property {string[]} [whitelist] - Array of specific telephone numbers that are always allowed
|
|
37
|
+
* @property {Function} [transform] - Custom transformation function for telephone number
|
|
38
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
39
|
+
* @property {Record<Locale, TelMessages>} [i18n] - Custom error messages for different locales
|
|
40
|
+
*/
|
|
41
|
+
export type TelOptions<IsRequired extends boolean = true> = {
|
|
42
|
+
required?: IsRequired
|
|
43
|
+
whitelist?: string[]
|
|
44
|
+
transform?: (value: string) => string
|
|
45
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
46
|
+
i18n?: Record<Locale, TelMessages>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type alias for telephone validation schema based on required flag
|
|
51
|
+
*
|
|
52
|
+
* @template IsRequired - Whether the field is required
|
|
53
|
+
* @typedef TelSchema
|
|
54
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
55
|
+
*/
|
|
56
|
+
export type TelSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates Taiwan landline telephone number format (Official 2024 rules)
|
|
60
|
+
*
|
|
61
|
+
* @param {string} value - The telephone number to validate
|
|
62
|
+
* @returns {boolean} True if the telephone number is valid
|
|
63
|
+
*
|
|
64
|
+
* @description
|
|
65
|
+
* Validates Taiwan landline telephone numbers according to the official 2024
|
|
66
|
+
* telecom numbering plan. Supports all Taiwan area codes and their specific
|
|
67
|
+
* number patterns.
|
|
68
|
+
*
|
|
69
|
+
* Supported area codes and formats:
|
|
70
|
+
* - 02: Taipei, New Taipei, Keelung - 8 digits (2&3&5~8+7D)
|
|
71
|
+
* - 03: Taoyuan, Hsinchu, Yilan, Hualien - 7 digits
|
|
72
|
+
* - 037: Miaoli - 6 digits (2~9+5D)
|
|
73
|
+
* - 04: Taichung, Changhua - 7 digits
|
|
74
|
+
* - 049: Nantou - 7 digits (2~9+6D)
|
|
75
|
+
* - 05: Yunlin, Chiayi - 7 digits
|
|
76
|
+
* - 06: Tainan - 7 digits
|
|
77
|
+
* - 07: Kaohsiung - 7 digits (2~9+6D)
|
|
78
|
+
* - 08: Pingtung - 7 digits (4&7&8+6D)
|
|
79
|
+
* - 082: Kinmen - 6 digits (2~5&7~9+5D)
|
|
80
|
+
* - 0826: Wuqiu - 5 digits (6+4D)
|
|
81
|
+
* - 0836: Matsu - 5 digits (2~9+4D)
|
|
82
|
+
* - 089: Taitung - 6 digits (2~9+5D)
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* validateTaiwanTel("0223456789") // true (Taipei area)
|
|
87
|
+
* validateTaiwanTel("0312345678") // true (Taoyuan area)
|
|
88
|
+
* validateTaiwanTel("037234567") // true (Miaoli area)
|
|
89
|
+
* validateTaiwanTel("082234567") // true (Kinmen area)
|
|
90
|
+
* validateTaiwanTel("02-2345-6789") // true (with separators)
|
|
91
|
+
* validateTaiwanTel("0812345678") // false (invalid for 08 area)
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
const validateTaiwanTel = (value: string): boolean => {
|
|
95
|
+
// Official Taiwan landline formats according to telecom numbering plan:
|
|
96
|
+
// 02: Taipei, New Taipei, Keelung - 8 digits (2&3&5~8+7D)
|
|
97
|
+
// 03: Taoyuan, Hsinchu, Yilan, Hualien - 7 digits
|
|
98
|
+
// 037: Miaoli - 6 digits (2~9+5D)
|
|
99
|
+
// 04: Taichung, Changhua - 7 digits
|
|
100
|
+
// 049: Nantou - 7 digits (2~9+6D)
|
|
101
|
+
// 05: Yunlin, Chiayi - 7 digits
|
|
102
|
+
// 06: Tainan - 7 digits
|
|
103
|
+
// 07: Kaohsiung - 7 digits (2~9+6D)
|
|
104
|
+
// 08: Pingtung - 7 digits (4&7&8+6D)
|
|
105
|
+
// 082: Kinmen - 6 digits (2~5&7~9+5D)
|
|
106
|
+
// 0826: Wuqiu - 5 digits (6+4D)
|
|
107
|
+
// 0836: Matsu - 5 digits (2~9+4D)
|
|
108
|
+
// 089: Taitung - 6 digits (2~9+5D)
|
|
109
|
+
|
|
110
|
+
// Remove common separators for validation
|
|
111
|
+
const cleanValue = value.replace(/[-\s]/g, "")
|
|
112
|
+
|
|
113
|
+
// Basic format: starts with 0, then area code, then number
|
|
114
|
+
if (!/^0\d{7,10}$/.test(cleanValue)) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check 4-digit area codes first
|
|
119
|
+
const areaCode4 = cleanValue.substring(0, 4)
|
|
120
|
+
if (areaCode4 === "0826") {
|
|
121
|
+
// Wuqiu: 0826 + 5 digits (6+4D), total 9 digits
|
|
122
|
+
return cleanValue.length === 9 && /^0826[6]\d{4}$/.test(cleanValue)
|
|
123
|
+
}
|
|
124
|
+
if (areaCode4 === "0836") {
|
|
125
|
+
// Matsu: 0836 + 5 digits (2~9+4D), total 9 digits
|
|
126
|
+
return cleanValue.length === 9 && /^0836[2-9]\d{4}$/.test(cleanValue)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check 3-digit area codes
|
|
130
|
+
const areaCode3 = cleanValue.substring(0, 3)
|
|
131
|
+
if (areaCode3 === "037") {
|
|
132
|
+
// Miaoli: 037 + 6 digits (2~9+5D), total 9 digits
|
|
133
|
+
return cleanValue.length === 9 && /^037[2-9]\d{5}$/.test(cleanValue)
|
|
134
|
+
}
|
|
135
|
+
if (areaCode3 === "049") {
|
|
136
|
+
// Nantou: 049 + 7 digits (2~9+6D), total 10 digits
|
|
137
|
+
return cleanValue.length === 10 && /^049[2-9]\d{6}$/.test(cleanValue)
|
|
138
|
+
}
|
|
139
|
+
if (areaCode3 === "082") {
|
|
140
|
+
// Kinmen: 082 + 6 digits (2~5&7~9+5D), total 9 digits
|
|
141
|
+
return cleanValue.length === 9 && /^082[2-57-9]\d{5}$/.test(cleanValue)
|
|
142
|
+
}
|
|
143
|
+
if (areaCode3 === "089") {
|
|
144
|
+
// Taitung: 089 + 6 digits (2~9+5D), total 9 digits
|
|
145
|
+
return cleanValue.length === 9 && /^089[2-9]\d{5}$/.test(cleanValue)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check 2-digit area codes
|
|
149
|
+
const areaCode2 = cleanValue.substring(0, 2)
|
|
150
|
+
|
|
151
|
+
if (areaCode2 === "02") {
|
|
152
|
+
// Taipei, New Taipei, Keelung: 02 + 8 digits (2&3&5~8+7D), total 10 digits
|
|
153
|
+
return cleanValue.length === 10 && /^02[235-8]\d{7}$/.test(cleanValue)
|
|
154
|
+
}
|
|
155
|
+
if (["03", "04", "05", "06"].includes(areaCode2)) {
|
|
156
|
+
// Taoyuan/Hsinchu/Yilan/Hualien (03), Taichung/Changhua (04),
|
|
157
|
+
// Yunlin/Chiayi (05), Tainan (06): 7 digits, total 9 digits
|
|
158
|
+
return cleanValue.length === 9
|
|
159
|
+
}
|
|
160
|
+
if (areaCode2 === "07") {
|
|
161
|
+
// Kaohsiung: 07 + 7 digits (2~9+6D), total 9 digits
|
|
162
|
+
return cleanValue.length === 9 && /^07[2-9]\d{6}$/.test(cleanValue)
|
|
163
|
+
}
|
|
164
|
+
if (areaCode2 === "08") {
|
|
165
|
+
// Pingtung: 08 + 7 digits (4&7&8+6D), total 9 digits
|
|
166
|
+
return cleanValue.length === 9 && /^08[478]\d{6}$/.test(cleanValue)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Creates a Zod schema for Taiwan landline telephone number validation
|
|
174
|
+
*
|
|
175
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
176
|
+
* @param {TelOptions<IsRequired>} [options] - Configuration options for telephone validation
|
|
177
|
+
* @returns {TelSchema<IsRequired>} Zod schema for telephone number validation
|
|
178
|
+
*
|
|
179
|
+
* @description
|
|
180
|
+
* Creates a comprehensive Taiwan landline telephone number validator with support for
|
|
181
|
+
* all Taiwan area codes according to the official 2024 telecom numbering plan.
|
|
182
|
+
*
|
|
183
|
+
* Features:
|
|
184
|
+
* - Complete Taiwan area code support (02, 03, 037, 04, 049, 05, 06, 07, 08, 082, 0826, 0836, 089)
|
|
185
|
+
* - Automatic separator handling (hyphens and spaces)
|
|
186
|
+
* - Area-specific number length and pattern validation
|
|
187
|
+
* - Whitelist functionality for specific allowed numbers
|
|
188
|
+
* - Automatic trimming and preprocessing
|
|
189
|
+
* - Custom transformation functions
|
|
190
|
+
* - Comprehensive internationalization
|
|
191
|
+
* - Optional field support
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* // Basic telephone number validation
|
|
196
|
+
* const basicSchema = tel()
|
|
197
|
+
* basicSchema.parse("0223456789") // ✓ Valid (Taipei)
|
|
198
|
+
* basicSchema.parse("0312345678") // ✓ Valid (Taoyuan)
|
|
199
|
+
* basicSchema.parse("02-2345-6789") // ✓ Valid (with separators)
|
|
200
|
+
* basicSchema.parse("0812345678") // ✗ Invalid (wrong format for 08)
|
|
201
|
+
*
|
|
202
|
+
* // With whitelist (only specific numbers allowed)
|
|
203
|
+
* const whitelistSchema = tel({
|
|
204
|
+
* whitelist: ["0223456789", "0312345678"]
|
|
205
|
+
* })
|
|
206
|
+
* whitelistSchema.parse("0223456789") // ✓ Valid (in whitelist)
|
|
207
|
+
* whitelistSchema.parse("0287654321") // ✗ Invalid (not in whitelist)
|
|
208
|
+
*
|
|
209
|
+
* // Optional telephone number
|
|
210
|
+
* const optionalSchema = tel({ required: false })
|
|
211
|
+
* optionalSchema.parse("") // ✓ Valid (returns null)
|
|
212
|
+
* optionalSchema.parse("0223456789") // ✓ Valid
|
|
213
|
+
*
|
|
214
|
+
* // With custom transformation (remove separators)
|
|
215
|
+
* const transformSchema = tel({
|
|
216
|
+
* transform: (value) => value.replace(/[^0-9]/g, '') // Keep only digits
|
|
217
|
+
* })
|
|
218
|
+
* transformSchema.parse("02-2345-6789") // ✓ Valid (separators removed)
|
|
219
|
+
* transformSchema.parse("02 2345 6789") // ✓ Valid (spaces removed)
|
|
220
|
+
*
|
|
221
|
+
* // With custom error messages
|
|
222
|
+
* const customSchema = tel({
|
|
223
|
+
* i18n: {
|
|
224
|
+
* en: { invalid: "Please enter a valid Taiwan landline number" },
|
|
225
|
+
* 'zh-TW': { invalid: "請輸入有效的台灣市話號碼" }
|
|
226
|
+
* }
|
|
227
|
+
* })
|
|
228
|
+
* ```
|
|
229
|
+
*
|
|
230
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
231
|
+
* @see {@link TelOptions} for all available configuration options
|
|
232
|
+
* @see {@link validateTaiwanTel} for validation logic details
|
|
233
|
+
*/
|
|
234
|
+
export function tel<IsRequired extends boolean = true>(options?: TelOptions<IsRequired>): TelSchema<IsRequired> {
|
|
235
|
+
const { required = true, whitelist, transform, defaultValue, i18n } = options ?? {}
|
|
236
|
+
|
|
237
|
+
// Set appropriate default value based on required flag
|
|
238
|
+
const actualDefaultValue = defaultValue ?? (required ? "" : null)
|
|
239
|
+
|
|
240
|
+
// Helper function to get custom message or fallback to default i18n
|
|
241
|
+
const getMessage = (key: keyof TelMessages, params?: Record<string, any>) => {
|
|
242
|
+
if (i18n) {
|
|
243
|
+
const currentLocale = getLocale()
|
|
244
|
+
const customMessages = i18n[currentLocale]
|
|
245
|
+
if (customMessages && customMessages[key]) {
|
|
246
|
+
const template = customMessages[key]!
|
|
247
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return t(`taiwan.tel.${key}`, params)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Preprocessing function
|
|
254
|
+
const preprocessFn = (val: unknown) => {
|
|
255
|
+
if (val === null || val === undefined) {
|
|
256
|
+
return actualDefaultValue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let processed = String(val).trim()
|
|
260
|
+
|
|
261
|
+
// If after trimming we have an empty string
|
|
262
|
+
if (processed === "") {
|
|
263
|
+
// If empty string is in allowlist, return it as is
|
|
264
|
+
if (whitelist && whitelist.includes("")) {
|
|
265
|
+
return ""
|
|
266
|
+
}
|
|
267
|
+
// If the field is optional and empty string not in allowlist, return default value
|
|
268
|
+
if (!required) {
|
|
269
|
+
return actualDefaultValue
|
|
270
|
+
}
|
|
271
|
+
// If a field is required, return the default value (will be validated later)
|
|
272
|
+
return actualDefaultValue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (transform) {
|
|
276
|
+
processed = transform(processed)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return processed
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
283
|
+
|
|
284
|
+
const schema = baseSchema.refine((val) => {
|
|
285
|
+
if (val === null) return true
|
|
286
|
+
|
|
287
|
+
// Required check
|
|
288
|
+
if (required && (val === "" || val === "null" || val === "undefined")) {
|
|
289
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (val === null) return true
|
|
293
|
+
if (!required && val === "") return true
|
|
294
|
+
|
|
295
|
+
// Allowlist check (if an allowlist is provided, only allow values in the allowlist)
|
|
296
|
+
if (whitelist && whitelist.length > 0) {
|
|
297
|
+
if (whitelist.includes(val)) {
|
|
298
|
+
return true
|
|
299
|
+
}
|
|
300
|
+
// If not in the allowlist, reject regardless of format
|
|
301
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("notInWhitelist"), path: [] }])
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Taiwan telephone format validation (only if no allowlist or allowlist is empty)
|
|
305
|
+
if (!validateTaiwanTel(val)) {
|
|
306
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return true
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
return schema as unknown as TelSchema<IsRequired>
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Utility function exported for external use
|
|
317
|
+
*
|
|
318
|
+
* @description
|
|
319
|
+
* The validation function can be used independently for telephone number validation
|
|
320
|
+
* without creating a full Zod schema.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* import { validateTaiwanTel } from './tel'
|
|
325
|
+
*
|
|
326
|
+
* // Direct validation
|
|
327
|
+
* const isValid = validateTaiwanTel("0223456789") // boolean
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export { validateTaiwanTel }
|