@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,425 @@
1
+ /**
2
+ * @fileoverview Taiwan National ID (身分證/居留證) validator for Zod Kit
3
+ *
4
+ * Provides validation for Taiwan National ID cards (身分證) and
5
+ * Resident Certificates (居留證) with support for both old and new formats.
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 national ID validation error messages
17
+ *
18
+ * @interface NationalIdMessages
19
+ * @property {string} [required] - Message when field is required but empty
20
+ * @property {string} [invalid] - Message when national ID format or checksum is invalid
21
+ */
22
+ export type NationalIdMessages = {
23
+ required?: string
24
+ invalid?: string
25
+ }
26
+
27
+ /**
28
+ * Types of Taiwan national identification documents
29
+ *
30
+ * @typedef {"citizen" | "resident" | "both"} NationalIdType
31
+ *
32
+ * Available types:
33
+ * - citizen: National ID card (身分證字號) for Taiwan citizens
34
+ * - resident: Resident certificate (居留證號) for foreign residents
35
+ * - both: Accept both citizen and resident IDs
36
+ */
37
+ export type NationalIdType =
38
+ | "citizen" // National ID card (身分證字號)
39
+ | "resident" // Resident certificate (居留證號)
40
+ | "both" // Both citizen and resident IDs accepted
41
+
42
+ /**
43
+ * Configuration options for Taiwan national ID validation
44
+ *
45
+ * @template IsRequired - Whether the field is required (affects return type)
46
+ *
47
+ * @interface NationalIdOptions
48
+ * @property {IsRequired} [required=true] - Whether the field is required
49
+ * @property {NationalIdType} [type="both"] - Type of ID to accept
50
+ * @property {boolean} [allowOldResident=true] - Whether to accept old-style resident certificates
51
+ * @property {Function} [transform] - Custom transformation function for ID
52
+ * @property {string | null} [defaultValue] - Default value when input is empty
53
+ * @property {Record<Locale, NationalIdMessages>} [i18n] - Custom error messages for different locales
54
+ */
55
+ export type NationalIdOptions<IsRequired extends boolean = true> = {
56
+ required?: IsRequired
57
+ type?: NationalIdType
58
+ allowOldResident?: boolean
59
+ transform?: (value: string) => string
60
+ defaultValue?: IsRequired extends true ? string : string | null
61
+ i18n?: Record<Locale, NationalIdMessages>
62
+ }
63
+
64
+ /**
65
+ * Type alias for national ID validation schema based on required flag
66
+ *
67
+ * @template IsRequired - Whether the field is required
68
+ * @typedef NationalIdSchema
69
+ * @description Returns ZodString if required, ZodNullable<ZodString> if optional
70
+ */
71
+ export type NationalIdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
72
+
73
+ /**
74
+ * Mapping of Taiwan city/county codes to their numeric values
75
+ *
76
+ * @constant {Record<string, number>} CITY_CODES
77
+ * @description Maps the first letter of Taiwan ID to corresponding numeric code
78
+ */
79
+ const CITY_CODES: Record<string, number> = {
80
+ 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17,
81
+ 'I': 34, 'J': 18, 'K': 19, 'L': 20, 'M': 21, 'N': 22, 'O': 35, 'P': 23,
82
+ 'Q': 24, 'R': 25, 'S': 26, 'T': 27, 'U': 28, 'V': 29, 'W': 32, 'X': 30,
83
+ 'Y': 31, 'Z': 33
84
+ }
85
+
86
+ /**
87
+ * Validates Taiwan citizen national ID card (身分證字號)
88
+ *
89
+ * @param {string} value - The citizen ID to validate
90
+ * @returns {boolean} True if the citizen ID is valid
91
+ *
92
+ * @description
93
+ * Validates Taiwan citizen ID format: 1 letter + 1 gender digit (1-2) + 8 digits
94
+ * Uses checksum algorithm with city code conversion and weighted sum.
95
+ *
96
+ * Format: [A-Z][1-2]XXXXXXXX
97
+ * - First letter: City/county code
98
+ * - Second digit: Gender (1=male, 2=female)
99
+ * - Last 8 digits: Serial number + checksum
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * validateCitizenId("A123456789") // true/false based on checksum
104
+ * validateCitizenId("A323456789") // false (invalid gender digit)
105
+ * ```
106
+ */
107
+ const validateCitizenId = (value: string): boolean => {
108
+ // 格式檢查:1個英文字母 + 9個數字
109
+ if (!/^[A-Z][1-2]\d{8}$/.test(value)) {
110
+ return false
111
+ }
112
+
113
+ const letter = value[0]
114
+ const digits = value.slice(1).split('').map(Number)
115
+
116
+ // 獲取縣市代碼
117
+ const cityCode = CITY_CODES[letter]
118
+ if (!cityCode) return false
119
+
120
+ // 計算校驗碼
121
+ const cityDigits = [Math.floor(cityCode / 10), cityCode % 10]
122
+ const coefficients = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1]
123
+
124
+ let sum = cityDigits[0] * coefficients[0] + cityDigits[1] * coefficients[1]
125
+ for (let i = 0; i < 8; i++) {
126
+ sum += digits[i] * coefficients[i + 2]
127
+ }
128
+
129
+ const checksum = (10 - (sum % 10)) % 10
130
+ return checksum === digits[8]
131
+ }
132
+
133
+ /**
134
+ * Validates old-style Taiwan resident certificate (舊式居留證號)
135
+ *
136
+ * @param {string} value - The old-style resident ID to validate
137
+ * @returns {boolean} True if the old-style resident ID is valid
138
+ *
139
+ * @description
140
+ * Validates old-style resident ID format: 1 letter + 1 gender letter + 8 digits
141
+ * Uses checksum algorithm with city code and gender code conversion.
142
+ *
143
+ * Format: [A-Z][ABCD]XXXXXXXX
144
+ * - First letter: City/county code
145
+ * - Second letter: Gender code (A/C=male, B/D=female)
146
+ * - Last 8 digits: Serial number + checksum
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * validateOldResidentId("AA12345678") // true/false based on checksum
151
+ * validateOldResidentId("AE12345678") // false (invalid gender letter)
152
+ * ```
153
+ */
154
+ const validateOldResidentId = (value: string): boolean => {
155
+ // 格式檢查:1個英文字母 + [AB或CD] + 8個數字
156
+ if (!/^[A-Z][ABCD]\d{8}$/.test(value)) {
157
+ return false
158
+ }
159
+
160
+ const letter = value[0]
161
+ const genderCode = value[1]
162
+ const digits = value.slice(2).split('').map(Number)
163
+
164
+ // 獲取縣市代碼
165
+ const cityCode = CITY_CODES[letter]
166
+ if (!cityCode) return false
167
+
168
+ // 性別代碼轉換
169
+ const genderValue = genderCode === 'A' || genderCode === 'C' ? 1 : 0
170
+
171
+ // 計算校驗碼
172
+ const cityDigits = [Math.floor(cityCode / 10), cityCode % 10]
173
+ const coefficients = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1]
174
+
175
+ let sum = cityDigits[0] * coefficients[0] + cityDigits[1] * coefficients[1] + genderValue * coefficients[2]
176
+ for (let i = 0; i < 7; i++) {
177
+ sum += digits[i] * coefficients[i + 3]
178
+ }
179
+
180
+ const checksum = (10 - (sum % 10)) % 10
181
+ return checksum === digits[7]
182
+ }
183
+
184
+ /**
185
+ * Validates new-style Taiwan resident certificate (新式居留證號)
186
+ *
187
+ * @param {string} value - The new-style resident ID to validate
188
+ * @returns {boolean} True if the new-style resident ID is valid
189
+ *
190
+ * @description
191
+ * Validates new-style resident ID format: 1 letter + 1 type digit + 8 digits
192
+ * Uses the same checksum algorithm as citizen IDs.
193
+ *
194
+ * Format: [A-Z][89]XXXXXXXX
195
+ * - First letter: City/county code
196
+ * - Second digit: Type indicator (8 or 9)
197
+ * - Last 8 digits: Serial number + checksum
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * validateNewResidentId("A812345678") // true/false based on checksum
202
+ * validateNewResidentId("A712345678") // false (invalid type digit)
203
+ * ```
204
+ */
205
+ const validateNewResidentId = (value: string): boolean => {
206
+ // 格式檢查:1個英文字母 + [89] + 1個數字[0-9] + 7個數字
207
+ if (!/^[A-Z][89]\d{8}$/.test(value)) {
208
+ return false
209
+ }
210
+
211
+ const letter = value[0]
212
+ const digits = value.slice(1).split('').map(Number)
213
+
214
+ // 獲取縣市代碼
215
+ const cityCode = CITY_CODES[letter]
216
+ if (!cityCode) return false
217
+
218
+ // 計算校驗碼 (與身分證字號相同邏輯)
219
+ const cityDigits = [Math.floor(cityCode / 10), cityCode % 10]
220
+ const coefficients = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1]
221
+
222
+ let sum = cityDigits[0] * coefficients[0] + cityDigits[1] * coefficients[1]
223
+ for (let i = 0; i < 8; i++) {
224
+ sum += digits[i] * coefficients[i + 2]
225
+ }
226
+
227
+ const checksum = (10 - (sum % 10)) % 10
228
+ return checksum === digits[8]
229
+ }
230
+
231
+ /**
232
+ * Main validation function for Taiwan national IDs
233
+ *
234
+ * @param {string} value - The national ID to validate
235
+ * @param {NationalIdType} [type="both"] - Type of ID to accept
236
+ * @param {boolean} [allowOldResident=true] - Whether to accept old-style resident certificates
237
+ * @returns {boolean} True if the national ID is valid
238
+ *
239
+ * @description
240
+ * Validates Taiwan national IDs based on the specified type and options.
241
+ * Supports citizen IDs, resident certificates (both old and new styles).
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * validateTaiwanNationalId("A123456789", "citizen") // Citizen ID only
246
+ * validateTaiwanNationalId("A812345678", "resident") // Resident ID only
247
+ * validateTaiwanNationalId("A123456789", "both") // Accept any valid format
248
+ * validateTaiwanNationalId("AA12345678", "both", false) // Reject old resident format
249
+ * ```
250
+ */
251
+ const validateTaiwanNationalId = (value: string, type: NationalIdType = "both", allowOldResident: boolean = true): boolean => {
252
+ if (!/^[A-Z].{9}$/.test(value)) {
253
+ return false
254
+ }
255
+
256
+ switch (type) {
257
+ case "citizen":
258
+ return validateCitizenId(value)
259
+ case "resident":
260
+ return (allowOldResident ? validateOldResidentId(value) : false) || validateNewResidentId(value)
261
+ case "both":
262
+ return validateCitizenId(value) || (allowOldResident ? validateOldResidentId(value) : false) || validateNewResidentId(value)
263
+ default:
264
+ return false
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Creates a Zod schema for Taiwan National ID validation
270
+ *
271
+ * @template IsRequired - Whether the field is required (affects return type)
272
+ * @param {NationalIdOptions<IsRequired>} [options] - Configuration options for national ID validation
273
+ * @returns {NationalIdSchema<IsRequired>} Zod schema for national ID validation
274
+ *
275
+ * @description
276
+ * Creates a comprehensive Taiwan National ID validator that supports both
277
+ * citizen IDs and resident certificates with configurable validation rules.
278
+ *
279
+ * Features:
280
+ * - Citizen ID validation (身分證字號)
281
+ * - Resident certificate validation (居留證號, old and new formats)
282
+ * - Configurable ID type acceptance
283
+ * - Automatic case conversion to uppercase
284
+ * - Checksum verification
285
+ * - Custom transformation functions
286
+ * - Comprehensive internationalization
287
+ * - Optional field support
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * // Accept any valid Taiwan ID
292
+ * const anyIdSchema = nationalId()
293
+ * anyIdSchema.parse("A123456789") // ✓ Valid citizen ID
294
+ * anyIdSchema.parse("A812345678") // ✓ Valid new resident ID
295
+ * anyIdSchema.parse("AA12345678") // ✓ Valid old resident ID
296
+ *
297
+ * // Citizen IDs only
298
+ * const citizenSchema = nationalId({ type: "citizen" })
299
+ * citizenSchema.parse("A123456789") // ✓ Valid
300
+ * citizenSchema.parse("A812345678") // ✗ Invalid (resident ID)
301
+ *
302
+ * // Resident IDs only (new format only)
303
+ * const residentSchema = nationalId({
304
+ * type: "resident",
305
+ * allowOldResident: false
306
+ * })
307
+ * residentSchema.parse("A812345678") // ✓ Valid
308
+ * residentSchema.parse("AA12345678") // ✗ Invalid (old format)
309
+ *
310
+ * // Optional with custom transformation
311
+ * const optionalSchema = nationalId({
312
+ * required: false,
313
+ * transform: (value) => value.replace(/[^A-Z0-9]/g, '') // Remove special chars
314
+ * })
315
+ *
316
+ * // With custom error messages
317
+ * const customSchema = nationalId({
318
+ * i18n: {
319
+ * en: { invalid: "Please enter a valid Taiwan National ID" },
320
+ * 'zh-TW': { invalid: "請輸入有效的身分證或居留證號碼" }
321
+ * }
322
+ * })
323
+ * ```
324
+ *
325
+ * @throws {z.ZodError} When validation fails with specific error messages
326
+ * @see {@link NationalIdOptions} for all available configuration options
327
+ * @see {@link NationalIdType} for supported ID types
328
+ * @see {@link validateTaiwanNationalId} for validation logic details
329
+ */
330
+ export function nationalId<IsRequired extends boolean = true>(options?: NationalIdOptions<IsRequired>): NationalIdSchema<IsRequired> {
331
+ const {
332
+ required = true,
333
+ type = "both",
334
+ allowOldResident = true,
335
+ transform,
336
+ defaultValue,
337
+ i18n
338
+ } = options ?? {}
339
+
340
+ // Set appropriate default value based on required flag
341
+ const actualDefaultValue = defaultValue ?? (required ? "" : null)
342
+
343
+ // Helper function to get custom message or fallback to default i18n
344
+ const getMessage = (key: keyof NationalIdMessages, params?: Record<string, any>) => {
345
+ if (i18n) {
346
+ const currentLocale = getLocale()
347
+ const customMessages = i18n[currentLocale]
348
+ if (customMessages && customMessages[key]) {
349
+ const template = customMessages[key]!
350
+ return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
351
+ }
352
+ }
353
+ return t(`taiwan.nationalId.${key}`, params)
354
+ }
355
+
356
+ // Preprocessing function
357
+ const preprocessFn = (val: unknown) => {
358
+ if (val === "" || val === null || val === undefined) {
359
+ return actualDefaultValue
360
+ }
361
+
362
+ let processed = String(val).trim().toUpperCase()
363
+
364
+ // If after trimming we have an empty string and the field is optional, return null
365
+ if (processed === "" && !required) {
366
+ return null
367
+ }
368
+
369
+ if (transform) {
370
+ processed = transform(processed)
371
+ }
372
+
373
+ return processed
374
+ }
375
+
376
+ const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
377
+
378
+ const schema = baseSchema.refine((val) => {
379
+ if (val === null) return true
380
+
381
+ // Required check
382
+ if (required && (val === "" || val === "null" || val === "undefined")) {
383
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
384
+ }
385
+
386
+ if (val === null) return true
387
+ if (!required && val === "") return true
388
+
389
+ // Taiwan National ID validation
390
+ if (!validateTaiwanNationalId(val, type, allowOldResident)) {
391
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
392
+ }
393
+
394
+ return true
395
+ })
396
+
397
+ return schema as unknown as NationalIdSchema<IsRequired>
398
+ }
399
+
400
+ /**
401
+ * Utility functions exported for external use
402
+ *
403
+ * @description
404
+ * These validation functions can be used independently for national ID validation
405
+ * without creating a full Zod schema. Useful for custom validation logic.
406
+ *
407
+ * @example
408
+ * ```typescript
409
+ * import {
410
+ * validateTaiwanNationalId,
411
+ * validateCitizenId,
412
+ * validateOldResidentId,
413
+ * validateNewResidentId
414
+ * } from './national-id'
415
+ *
416
+ * // General validation
417
+ * const isValid = validateTaiwanNationalId("A123456789", "both") // boolean
418
+ *
419
+ * // Specific format validation
420
+ * const isCitizen = validateCitizenId("A123456789") // boolean
421
+ * const isOldResident = validateOldResidentId("AA12345678") // boolean
422
+ * const isNewResident = validateNewResidentId("A812345678") // boolean
423
+ * ```
424
+ */
425
+ export { validateTaiwanNationalId, validateCitizenId, validateOldResidentId, validateNewResidentId }