@hy_ong/zod-kit 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -97
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +3127 -146
  7. package/dist/index.d.cts +3021 -25
  8. package/dist/index.d.ts +3021 -25
  9. package/dist/index.js +3081 -144
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +161 -25
  14. package/src/i18n/locales/zh-TW.json +165 -26
  15. package/src/index.ts +17 -7
  16. package/src/validators/common/boolean.ts +191 -0
  17. package/src/validators/common/date.ts +299 -0
  18. package/src/validators/common/datetime.ts +673 -0
  19. package/src/validators/common/email.ts +313 -0
  20. package/src/validators/common/file.ts +384 -0
  21. package/src/validators/common/id.ts +471 -0
  22. package/src/validators/common/number.ts +319 -0
  23. package/src/validators/common/password.ts +386 -0
  24. package/src/validators/common/text.ts +271 -0
  25. package/src/validators/common/time.ts +600 -0
  26. package/src/validators/common/url.ts +347 -0
  27. package/src/validators/taiwan/business-id.ts +262 -0
  28. package/src/validators/taiwan/fax.ts +327 -0
  29. package/src/validators/taiwan/mobile.ts +242 -0
  30. package/src/validators/taiwan/national-id.ts +425 -0
  31. package/src/validators/taiwan/postal-code.ts +1049 -0
  32. package/src/validators/taiwan/tel.ts +330 -0
  33. package/tests/common/boolean.test.ts +340 -92
  34. package/tests/common/date.test.ts +458 -0
  35. package/tests/common/datetime.test.ts +693 -0
  36. package/tests/common/email.test.ts +232 -60
  37. package/tests/common/file.test.ts +479 -0
  38. package/tests/common/id.test.ts +535 -0
  39. package/tests/common/number.test.ts +230 -60
  40. package/tests/common/password.test.ts +271 -44
  41. package/tests/common/text.test.ts +210 -13
  42. package/tests/common/time.test.ts +528 -0
  43. package/tests/common/url.test.ts +492 -67
  44. package/tests/taiwan/business-id.test.ts +240 -0
  45. package/tests/taiwan/fax.test.ts +463 -0
  46. package/tests/taiwan/mobile.test.ts +373 -0
  47. package/tests/taiwan/national-id.test.ts +435 -0
  48. package/tests/taiwan/postal-code.test.ts +705 -0
  49. package/tests/taiwan/tel.test.ts +467 -0
  50. package/eslint.config.mjs +0 -10
  51. package/src/common/boolean.ts +0 -36
  52. package/src/common/date.ts +0 -43
  53. package/src/common/email.ts +0 -44
  54. package/src/common/integer.ts +0 -46
  55. package/src/common/number.ts +0 -37
  56. package/src/common/password.ts +0 -33
  57. package/src/common/text.ts +0 -34
  58. package/src/common/url.ts +0 -37
  59. package/tests/common/integer.test.ts +0 -90
@@ -0,0 +1,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 }