@hy_ong/zod-kit 0.0.5 → 0.1.0

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 (46) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/README.md +465 -97
  3. package/dist/index.cjs +1690 -179
  4. package/dist/index.d.cts +2791 -28
  5. package/dist/index.d.ts +2791 -28
  6. package/dist/index.js +1672 -178
  7. package/package.json +2 -1
  8. package/src/i18n/locales/en.json +62 -0
  9. package/src/i18n/locales/zh-TW.json +62 -0
  10. package/src/index.ts +4 -0
  11. package/src/validators/common/boolean.ts +101 -4
  12. package/src/validators/common/date.ts +141 -6
  13. package/src/validators/common/datetime.ts +680 -0
  14. package/src/validators/common/email.ts +120 -4
  15. package/src/validators/common/file.ts +391 -0
  16. package/src/validators/common/id.ts +230 -18
  17. package/src/validators/common/number.ts +132 -4
  18. package/src/validators/common/password.ts +187 -8
  19. package/src/validators/common/text.ts +130 -6
  20. package/src/validators/common/time.ts +607 -0
  21. package/src/validators/common/url.ts +153 -6
  22. package/src/validators/taiwan/business-id.ts +138 -9
  23. package/src/validators/taiwan/fax.ts +164 -10
  24. package/src/validators/taiwan/mobile.ts +151 -10
  25. package/src/validators/taiwan/national-id.ts +233 -17
  26. package/src/validators/taiwan/postal-code.ts +1048 -0
  27. package/src/validators/taiwan/tel.ts +167 -10
  28. package/tests/common/boolean.test.ts +38 -38
  29. package/tests/common/date.test.ts +65 -65
  30. package/tests/common/datetime.test.ts +675 -0
  31. package/tests/common/email.test.ts +24 -28
  32. package/tests/common/file.test.ts +475 -0
  33. package/tests/common/id.test.ts +80 -113
  34. package/tests/common/number.test.ts +24 -25
  35. package/tests/common/password.test.ts +28 -35
  36. package/tests/common/text.test.ts +36 -37
  37. package/tests/common/time.test.ts +510 -0
  38. package/tests/common/url.test.ts +67 -67
  39. package/tests/taiwan/business-id.test.ts +22 -22
  40. package/tests/taiwan/fax.test.ts +33 -42
  41. package/tests/taiwan/mobile.test.ts +32 -41
  42. package/tests/taiwan/national-id.test.ts +31 -31
  43. package/tests/taiwan/postal-code.test.ts +751 -0
  44. package/tests/taiwan/tel.test.ts +33 -42
  45. package/debug.js +0 -21
  46. package/debug.ts +0 -16
@@ -1,7 +1,40 @@
1
+ /**
2
+ * @fileoverview URL validator for Zod Kit
3
+ *
4
+ * Provides comprehensive URL validation with protocol filtering, domain control,
5
+ * port validation, path constraints, and localhost handling.
6
+ *
7
+ * @author Ong Hoe Yuan
8
+ * @version 0.0.5
9
+ */
10
+
1
11
  import { z, ZodNullable, ZodString } from "zod"
2
12
  import { t } from "../../i18n"
3
13
  import { getLocale, type Locale } from "../../config"
4
14
 
15
+ /**
16
+ * Type definition for URL validation error messages
17
+ *
18
+ * @interface UrlMessages
19
+ * @property {string} [required] - Message when field is required but empty
20
+ * @property {string} [invalid] - Message when URL format is invalid
21
+ * @property {string} [min] - Message when URL is too short
22
+ * @property {string} [max] - Message when URL is too long
23
+ * @property {string} [includes] - Message when URL doesn't contain required string
24
+ * @property {string} [excludes] - Message when URL contains forbidden string
25
+ * @property {string} [protocol] - Message when protocol is not allowed
26
+ * @property {string} [domain] - Message when domain is not allowed
27
+ * @property {string} [domainBlacklist] - Message when domain is blacklisted
28
+ * @property {string} [port] - Message when port is not allowed
29
+ * @property {string} [pathStartsWith] - Message when path doesn't start with required string
30
+ * @property {string} [pathEndsWith] - Message when path doesn't end with required string
31
+ * @property {string} [hasQuery] - Message when query parameters are required
32
+ * @property {string} [noQuery] - Message when query parameters are forbidden
33
+ * @property {string} [hasFragment] - Message when fragment is required
34
+ * @property {string} [noFragment] - Message when fragment is forbidden
35
+ * @property {string} [localhost] - Message when localhost is forbidden
36
+ * @property {string} [noLocalhost] - Message when localhost is required
37
+ */
5
38
  export type UrlMessages = {
6
39
  required?: string
7
40
  invalid?: string
@@ -23,8 +56,35 @@ export type UrlMessages = {
23
56
  noLocalhost?: string
24
57
  }
25
58
 
59
+ /**
60
+ * Configuration options for URL validation
61
+ *
62
+ * @template IsRequired - Whether the field is required (affects return type)
63
+ *
64
+ * @interface UrlOptions
65
+ * @property {IsRequired} [required=true] - Whether the field is required
66
+ * @property {number} [min] - Minimum length of URL
67
+ * @property {number} [max] - Maximum length of URL
68
+ * @property {string} [includes] - String that must be included in URL
69
+ * @property {string | string[]} [excludes] - String(s) that must not be included
70
+ * @property {string[]} [protocols] - Allowed protocols (e.g., ["https", "http"])
71
+ * @property {string[]} [allowedDomains] - Domains that are allowed
72
+ * @property {string[]} [blockedDomains] - Domains that are blocked
73
+ * @property {number[]} [allowedPorts] - Ports that are allowed
74
+ * @property {number[]} [blockedPorts] - Ports that are blocked
75
+ * @property {string} [pathStartsWith] - Path must start with this string
76
+ * @property {string} [pathEndsWith] - Path must end with this string
77
+ * @property {boolean} [mustHaveQuery] - Whether URL must have query parameters
78
+ * @property {boolean} [mustNotHaveQuery] - Whether URL must not have query parameters
79
+ * @property {boolean} [mustHaveFragment] - Whether URL must have fragment
80
+ * @property {boolean} [mustNotHaveFragment] - Whether URL must not have fragment
81
+ * @property {boolean} [allowLocalhost=true] - Whether to allow localhost URLs
82
+ * @property {boolean} [blockLocalhost] - Whether to explicitly block localhost URLs
83
+ * @property {Function} [transform] - Custom transformation function for URL strings
84
+ * @property {string | null} [defaultValue] - Default value when input is empty
85
+ * @property {Record<Locale, UrlMessages>} [i18n] - Custom error messages for different locales
86
+ */
26
87
  export type UrlOptions<IsRequired extends boolean = true> = {
27
- required?: IsRequired
28
88
  min?: number
29
89
  max?: number
30
90
  includes?: string
@@ -47,11 +107,96 @@ export type UrlOptions<IsRequired extends boolean = true> = {
47
107
  i18n?: Record<Locale, UrlMessages>
48
108
  }
49
109
 
110
+ /**
111
+ * Type alias for URL validation schema based on required flag
112
+ *
113
+ * @template IsRequired - Whether the field is required
114
+ * @typedef UrlSchema
115
+ * @description Returns ZodString if required, ZodNullable<ZodString> if optional
116
+ */
50
117
  export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
51
118
 
52
- export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRequired>): UrlSchema<IsRequired> {
119
+ /**
120
+ * Creates a Zod schema for URL validation with comprehensive constraints
121
+ *
122
+ * @template IsRequired - Whether the field is required (affects return type)
123
+ * @param {IsRequired} [required=false] - Whether the field is required
124
+ * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
125
+ * @returns {UrlSchema<IsRequired>} Zod schema for URL validation
126
+ *
127
+ * @description
128
+ * Creates a comprehensive URL validator with protocol filtering, domain control,
129
+ * port validation, path constraints, and localhost handling.
130
+ *
131
+ * Features:
132
+ * - RFC-compliant URL format validation
133
+ * - Protocol whitelist/blacklist (http, https, ftp, etc.)
134
+ * - Domain whitelist/blacklist with subdomain support
135
+ * - Port validation and filtering
136
+ * - Path prefix/suffix validation
137
+ * - Query parameter requirements
138
+ * - Fragment requirements
139
+ * - Localhost detection and control
140
+ * - Length validation
141
+ * - Content inclusion/exclusion
142
+ * - Custom transformation functions
143
+ * - Comprehensive internationalization
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * // Basic URL validation
148
+ * const basicSchema = url() // optional by default
149
+ * basicSchema.parse("https://example.com") // ✓ Valid
150
+ * basicSchema.parse(null) // ✓ Valid (optional)
151
+ *
152
+ * // Required validation
153
+ * const requiredSchema = parse("https://example.com") // ✓ Valid
154
+ (true)
155
+ * requiredSchema.parse(null) // ✗ Invalid (required)
156
+ *
157
+ *
158
+ * // HTTPS only
159
+ * const httpsSchema = url(false, { protocols: ["https"] })
160
+ * httpsSchema.parse("https://example.com") // ✓ Valid
161
+ * httpsSchema.parse("http://example.com") // ✗ Invalid
162
+ *
163
+ * // Domain restriction
164
+ * const domainSchema = url(false, {
165
+ * allowedDomains: ["company.com", "trusted.org"]
166
+ * })
167
+ * domainSchema.parse("https://app.company.com") // ✓ Valid (subdomain)
168
+ * domainSchema.parse("https://example.com") // ✗ Invalid
169
+ *
170
+ * // Block localhost
171
+ * const noLocalhostSchema = url(false, { blockLocalhost: true })
172
+ * noLocalhostSchema.parse("https://example.com") // ✓ Valid
173
+ * noLocalhostSchema.parse("http://localhost:3000") // ✗ Invalid
174
+ *
175
+ * // API endpoints with path requirements
176
+ * const apiSchema = url(false, {
177
+ * pathStartsWith: "/api/",
178
+ * mustHaveQuery: true
179
+ * })
180
+ * apiSchema.parse("https://api.com/api/users?page=1") // ✓ Valid
181
+ *
182
+ * // Port restrictions
183
+ * const portSchema = url(false, {
184
+ * allowedPorts: [80, 443, 8080]
185
+ * })
186
+ * portSchema.parse("https://example.com:443") // ✓ Valid
187
+ * portSchema.parse("https://example.com:3000") // ✗ Invalid
188
+ *
189
+ * // Optional with default
190
+ * const optionalSchema = url(false, {
191
+ * defaultValue: null
192
+ * })
193
+ * ```
194
+ *
195
+ * @throws {z.ZodError} When validation fails with specific error messages
196
+ * @see {@link UrlOptions} for all available configuration options
197
+ */
198
+ export function url<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<UrlOptions<IsRequired>, 'required'>): UrlSchema<IsRequired> {
53
199
  const {
54
- required = true,
55
200
  min,
56
201
  max,
57
202
  includes,
@@ -74,7 +219,9 @@ export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRe
74
219
  i18n,
75
220
  } = options ?? {}
76
221
 
77
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
222
+ const isRequired = required ?? false as IsRequired
223
+
224
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
78
225
 
79
226
  // Helper function to get custom message or fallback to default i18n
80
227
  const getMessage = (key: keyof UrlMessages, params?: Record<string, any>) => {
@@ -104,13 +251,13 @@ export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRe
104
251
  return processed
105
252
  }
106
253
 
107
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
254
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
108
255
 
109
256
  const schema = baseSchema.refine((val) => {
110
257
  if (val === null) return true
111
258
 
112
259
  // Required check
113
- if (required && (val === "" || val === "null" || val === "undefined")) {
260
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
114
261
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
115
262
  }
116
263
 
@@ -1,22 +1,79 @@
1
+ /**
2
+ * @fileoverview Taiwan Business ID (統一編號) validator for Zod Kit
3
+ *
4
+ * Provides validation for Taiwan Business Identification Numbers (統一編號) with
5
+ * support for both new (2023+) and legacy validation rules.
6
+ *
7
+ * @author Ong Hoe Yuan
8
+ * @version 0.0.5
9
+ */
10
+
1
11
  import { z, ZodNullable, ZodString } from "zod"
2
12
  import { t } from "../../i18n"
3
13
  import { getLocale, type Locale } from "../../config"
4
14
 
15
+ /**
16
+ * Type definition for business ID validation error messages
17
+ *
18
+ * @interface BusinessIdMessages
19
+ * @property {string} [required] - Message when field is required but empty
20
+ * @property {string} [invalid] - Message when business ID format or checksum is invalid
21
+ */
5
22
  export type BusinessIdMessages = {
6
23
  required?: string
7
24
  invalid?: string
8
25
  }
9
26
 
27
+ /**
28
+ * Configuration options for Taiwan business ID validation
29
+ *
30
+ * @template IsRequired - Whether the field is required (affects return type)
31
+ *
32
+ * @interface BusinessIdOptions
33
+ * @property {IsRequired} [required=true] - Whether the field is required
34
+ * @property {Function} [transform] - Custom transformation function for business ID
35
+ * @property {string | null} [defaultValue] - Default value when input is empty
36
+ * @property {Record<Locale, BusinessIdMessages>} [i18n] - Custom error messages for different locales
37
+ */
10
38
  export type BusinessIdOptions<IsRequired extends boolean = true> = {
11
- required?: IsRequired
12
39
  transform?: (value: string) => string
13
40
  defaultValue?: IsRequired extends true ? string : string | null
14
41
  i18n?: Record<Locale, BusinessIdMessages>
15
42
  }
16
43
 
44
+ /**
45
+ * Type alias for business ID validation schema based on required flag
46
+ *
47
+ * @template IsRequired - Whether the field is required
48
+ * @typedef BusinessIdSchema
49
+ * @description Returns ZodString if required, ZodNullable<ZodString> if optional
50
+ */
17
51
  export type BusinessIdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
18
52
 
19
- // Taiwan Business ID (統一編號) validation
53
+ /**
54
+ * Validates Taiwan Business Identification Number (統一編號)
55
+ *
56
+ * @param {string} value - The business ID to validate
57
+ * @returns {boolean} True if the business ID is valid
58
+ *
59
+ * @description
60
+ * Validates Taiwan Business ID using both new (2023+) and legacy validation rules.
61
+ * The validation includes format checking (8 digits) and checksum verification.
62
+ *
63
+ * Validation rules:
64
+ * 1. Must be exactly 8 digits
65
+ * 2. Weighted sum calculation using coefficients [1,2,1,2,1,2,4] for first 7 digits
66
+ * 3. New rules (2023+): Sum + 8th digit must be divisible by 5
67
+ * 4. Legacy rules: Sum + 8th digit must be divisible by 10
68
+ * 5. Special case: If 7th digit is 7, try alternative calculation with +1
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * validateTaiwanBusinessId("12345675") // true (if valid checksum)
73
+ * validateTaiwanBusinessId("1234567") // false (not 8 digits)
74
+ * validateTaiwanBusinessId("abcd1234") // false (not all digits)
75
+ * ```
76
+ */
20
77
  const validateTaiwanBusinessId = (value: string): boolean => {
21
78
  // Must be exactly 8 digits
22
79
  if (!/^\d{8}$/.test(value)) {
@@ -68,16 +125,74 @@ const validateTaiwanBusinessId = (value: string): boolean => {
68
125
  return false
69
126
  }
70
127
 
71
- export function businessId<IsRequired extends boolean = true>(options?: BusinessIdOptions<IsRequired>): BusinessIdSchema<IsRequired> {
128
+ /**
129
+ * Creates a Zod schema for Taiwan Business ID validation
130
+ *
131
+ * @template IsRequired - Whether the field is required (affects return type)
132
+ * @param {BusinessIdOptions<IsRequired>} [options] - Configuration options for business ID validation
133
+ * @returns {BusinessIdSchema<IsRequired>} Zod schema for business ID validation
134
+ *
135
+ * @description
136
+ * Creates a comprehensive Taiwan Business ID validator that validates the format
137
+ * and checksum according to Taiwan government specifications.
138
+ *
139
+ * Features:
140
+ * - 8-digit format validation
141
+ * - Checksum verification (supports both new 2023+ and legacy rules)
142
+ * - Automatic trimming and preprocessing
143
+ * - Custom transformation functions
144
+ * - Comprehensive internationalization
145
+ * - Optional field support
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * // Basic business ID validation
150
+ * const basicSchema = businessId() // optional by default
151
+ * basicSchema.parse("12345675") // ✓ Valid (if checksum correct)
152
+ * basicSchema.parse(null) // ✓ Valid (optional)
153
+ *
154
+ * // Required validation
155
+ * const requiredSchema = parse("12345675") // ✓ Valid (if checksum correct)
156
+ (true)
157
+ * requiredSchema.parse(null) // ✗ Invalid (required)
158
+ *
159
+ * basicSchema.parse("1234567") // ✗ Invalid (not 8 digits)
160
+ *
161
+ * // Optional business ID
162
+ * const optionalSchema = businessId(false)
163
+ * optionalSchema.parse("") // ✓ Valid (returns null)
164
+ * optionalSchema.parse("12345675") // ✓ Valid (if checksum correct)
165
+ *
166
+ * // With custom transformation
167
+ * const transformSchema = businessId(false, {
168
+ * transform: (value) => value.replace(/[^0-9]/g, '') // Remove non-digits
169
+ * })
170
+ * transformSchema.parse("1234-5675") // ✓ Valid (if checksum correct after cleaning)
171
+ *
172
+ * // With custom error messages
173
+ * const customSchema = businessId(false, {
174
+ * i18n: {
175
+ * en: { invalid: "Please enter a valid Taiwan Business ID" },
176
+ * 'zh-TW': { invalid: "請輸入有效的統一編號" }
177
+ * }
178
+ * })
179
+ * ```
180
+ *
181
+ * @throws {z.ZodError} When validation fails with specific error messages
182
+ * @see {@link BusinessIdOptions} for all available configuration options
183
+ * @see {@link validateTaiwanBusinessId} for validation logic details
184
+ */
185
+ export function businessId<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<BusinessIdOptions<IsRequired>, 'required'>): BusinessIdSchema<IsRequired> {
72
186
  const {
73
- required = true,
74
187
  transform,
75
188
  defaultValue,
76
189
  i18n
77
190
  } = options ?? {}
78
191
 
192
+ const isRequired = required ?? false as IsRequired
193
+
79
194
  // Set appropriate default value based on required flag
80
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
195
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
81
196
 
82
197
  // Helper function to get custom message or fallback to default i18n
83
198
  const getMessage = (key: keyof BusinessIdMessages, params?: Record<string, any>) => {
@@ -112,18 +227,18 @@ export function businessId<IsRequired extends boolean = true>(options?: Business
112
227
  return processed
113
228
  }
114
229
 
115
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
230
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
116
231
 
117
232
  const schema = baseSchema.refine((val) => {
118
233
  if (val === null) return true
119
234
 
120
235
  // Required check
121
- if (required && (val === "" || val === "null" || val === "undefined")) {
236
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
122
237
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
123
238
  }
124
239
 
125
240
  if (val === null) return true
126
- if (!required && val === "") return true
241
+ if (!isRequired && val === "") return true
127
242
 
128
243
  // Taiwan Business ID format validation (8 digits + checksum)
129
244
  if (!validateTaiwanBusinessId(val)) {
@@ -136,5 +251,19 @@ export function businessId<IsRequired extends boolean = true>(options?: Business
136
251
  return schema as unknown as BusinessIdSchema<IsRequired>
137
252
  }
138
253
 
139
- // Export utility function for external use
254
+ /**
255
+ * Utility function exported for external use
256
+ *
257
+ * @description
258
+ * The validation function can be used independently for business ID validation
259
+ * without creating a full Zod schema.
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * import { validateTaiwanBusinessId } from './business-id'
264
+ *
265
+ * // Direct validation
266
+ * const isValid = validateTaiwanBusinessId("12345675") // boolean
267
+ * ```
268
+ */
140
269
  export { validateTaiwanBusinessId }
@@ -1,24 +1,93 @@
1
+ /**
2
+ * @fileoverview Taiwan Fax Number validator for Zod Kit
3
+ *
4
+ * Provides validation for Taiwan fax numbers according to the official 2024
5
+ * telecom numbering plan. Uses the same format as landline telephone numbers.
6
+ *
7
+ * @author Ong Hoe Yuan
8
+ * @version 0.0.5
9
+ */
10
+
1
11
  import { z, ZodNullable, ZodString } from "zod"
2
12
  import { t } from "../../i18n"
3
13
  import { getLocale, type Locale } from "../../config"
4
14
 
15
+ /**
16
+ * Type definition for fax number validation error messages
17
+ *
18
+ * @interface FaxMessages
19
+ * @property {string} [required] - Message when field is required but empty
20
+ * @property {string} [invalid] - Message when fax number format is invalid
21
+ * @property {string} [notInWhitelist] - Message when fax number is not in whitelist
22
+ */
5
23
  export type FaxMessages = {
6
24
  required?: string
7
25
  invalid?: string
8
26
  notInWhitelist?: string
9
27
  }
10
28
 
29
+ /**
30
+ * Configuration options for Taiwan fax number validation
31
+ *
32
+ * @template IsRequired - Whether the field is required (affects return type)
33
+ *
34
+ * @interface FaxOptions
35
+ * @property {IsRequired} [required=true] - Whether the field is required
36
+ * @property {string[]} [whitelist] - Array of specific fax numbers that are always allowed
37
+ * @property {Function} [transform] - Custom transformation function for fax number
38
+ * @property {string | null} [defaultValue] - Default value when input is empty
39
+ * @property {Record<Locale, FaxMessages>} [i18n] - Custom error messages for different locales
40
+ */
11
41
  export type FaxOptions<IsRequired extends boolean = true> = {
12
- required?: IsRequired
13
42
  whitelist?: string[]
14
43
  transform?: (value: string) => string
15
44
  defaultValue?: IsRequired extends true ? string : string | null
16
45
  i18n?: Record<Locale, FaxMessages>
17
46
  }
18
47
 
48
+ /**
49
+ * Type alias for fax number validation schema based on required flag
50
+ *
51
+ * @template IsRequired - Whether the field is required
52
+ * @typedef FaxSchema
53
+ * @description Returns ZodString if required, ZodNullable<ZodString> if optional
54
+ */
19
55
  export type FaxSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
20
56
 
21
- // Taiwan fax number validation (Official 2024 rules - same as landline)
57
+ /**
58
+ * Validates Taiwan fax number format (Official 2024 rules - same as landline)
59
+ *
60
+ * @param {string} value - The fax number to validate
61
+ * @returns {boolean} True if the fax number is valid
62
+ *
63
+ * @description
64
+ * Validates Taiwan fax numbers according to the official 2024 telecom numbering plan.
65
+ * Fax numbers follow the same format as landline telephone numbers in Taiwan.
66
+ *
67
+ * Supported area codes and formats (same as landline):
68
+ * - 02: Taipei, New Taipei, Keelung - 8 digits (2&3&5~8+7D)
69
+ * - 03: Taoyuan, Hsinchu, Yilan, Hualien - 7 digits
70
+ * - 037: Miaoli - 6 digits (2~9+5D)
71
+ * - 04: Taichung, Changhua - 7 digits
72
+ * - 049: Nantou - 7 digits (2~9+6D)
73
+ * - 05: Yunlin, Chiayi - 7 digits
74
+ * - 06: Tainan - 7 digits
75
+ * - 07: Kaohsiung - 7 digits (2~9+6D)
76
+ * - 08: Pingtung - 7 digits (4&7&8+6D)
77
+ * - 082: Kinmen - 6 digits (2~5&7~9+5D)
78
+ * - 0826: Wuqiu - 5 digits (6+4D)
79
+ * - 0836: Matsu - 5 digits (2~9+4D)
80
+ * - 089: Taitung - 6 digits (2~9+5D)
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * validateTaiwanFax("0223456789") // true (Taipei area)
85
+ * validateTaiwanFax("0312345678") // true (Taoyuan area)
86
+ * validateTaiwanFax("037234567") // true (Miaoli area)
87
+ * validateTaiwanFax("02-2345-6789") // true (with separators)
88
+ * validateTaiwanFax("0812345678") // false (invalid for 08 area)
89
+ * ```
90
+ */
22
91
  const validateTaiwanFax = (value: string): boolean => {
23
92
  // Official Taiwan fax formats according to telecom numbering plan (same as landline):
24
93
  // 02: Taipei, New Taipei, Keelung - 8 digits (2&3&5~8+7D)
@@ -97,11 +166,82 @@ const validateTaiwanFax = (value: string): boolean => {
97
166
  return false
98
167
  }
99
168
 
100
- export function fax<IsRequired extends boolean = true>(options?: FaxOptions<IsRequired>): FaxSchema<IsRequired> {
101
- const { required = true, whitelist, transform, defaultValue, i18n } = options ?? {}
169
+ /**
170
+ * Creates a Zod schema for Taiwan fax number validation
171
+ *
172
+ * @template IsRequired - Whether the field is required (affects return type)
173
+ * @param {IsRequired} [required=false] - Whether the field is required
174
+ * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
175
+ * @returns {FaxSchema<IsRequired>} Zod schema for fax number validation
176
+ *
177
+ * @description
178
+ * Creates a comprehensive Taiwan fax number validator with support for all Taiwan
179
+ * area codes. Fax numbers follow the same format as landline telephone numbers.
180
+ *
181
+ * Features:
182
+ * - Complete Taiwan area code support (same as landline)
183
+ * - Automatic separator handling (hyphens and spaces)
184
+ * - Area-specific number length and pattern validation
185
+ * - Whitelist functionality for specific allowed numbers
186
+ * - Automatic trimming and preprocessing
187
+ * - Custom transformation functions
188
+ * - Comprehensive internationalization
189
+ * - Optional field support
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * // Basic fax number validation
194
+ * const basicSchema = fax() // optional by default
195
+ * basicSchema.parse("0223456789") // ✓ Valid (Taipei)
196
+ * basicSchema.parse(null) // ✓ Valid (optional)
197
+ *
198
+ * // Required validation
199
+ * const requiredSchema = parse("0223456789") // ✓ Valid (Taipei)
200
+ (true)
201
+ * requiredSchema.parse(null) // ✗ Invalid (required)
202
+ *
203
+ * basicSchema.parse("0312345678") // ✓ Valid (Taoyuan)
204
+ * basicSchema.parse("02-2345-6789") // ✓ Valid (with separators)
205
+ * basicSchema.parse("0812345678") // ✗ Invalid (wrong format for 08)
206
+ *
207
+ * // With whitelist (only specific numbers allowed)
208
+ * const whitelistSchema = fax(false, {
209
+ * whitelist: ["0223456789", "0312345678"]
210
+ * })
211
+ * whitelistSchema.parse("0223456789") // ✓ Valid (in whitelist)
212
+ * whitelistSchema.parse("0287654321") // ✗ Invalid (not in whitelist)
213
+ *
214
+ * // Optional fax number
215
+ * const optionalSchema = fax(false)
216
+ * optionalSchema.parse("") // ✓ Valid (returns null)
217
+ * optionalSchema.parse("0223456789") // ✓ Valid
218
+ *
219
+ * // With custom transformation
220
+ * const transformSchema = fax(false, {
221
+ * transform: (value) => value.replace(/[^0-9]/g, '') // Keep only digits
222
+ * })
223
+ * transformSchema.parse("02-2345-6789") // ✓ Valid (separators removed)
224
+ *
225
+ * // With custom error messages
226
+ * const customSchema = fax(false, {
227
+ * i18n: {
228
+ * en: { invalid: "Please enter a valid Taiwan fax number" },
229
+ * 'zh-TW': { invalid: "請輸入有效的台灣傳真號碼" }
230
+ * }
231
+ * })
232
+ * ```
233
+ *
234
+ * @throws {z.ZodError} When validation fails with specific error messages
235
+ * @see {@link FaxOptions} for all available configuration options
236
+ * @see {@link validateTaiwanFax} for validation logic details
237
+ */
238
+ export function fax<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<FaxOptions<IsRequired>, 'required'>): FaxSchema<IsRequired> {
239
+ const { whitelist, transform, defaultValue, i18n } = options ?? {}
240
+
241
+ const isRequired = required ?? false as IsRequired
102
242
 
103
243
  // Set appropriate default value based on required flag
104
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
244
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
105
245
 
106
246
  // Helper function to get custom message or fallback to default i18n
107
247
  const getMessage = (key: keyof FaxMessages, params?: Record<string, any>) => {
@@ -131,7 +271,7 @@ export function fax<IsRequired extends boolean = true>(options?: FaxOptions<IsRe
131
271
  return ""
132
272
  }
133
273
  // If the field is optional and empty string not in allowlist, return default value
134
- if (!required) {
274
+ if (!isRequired) {
135
275
  return actualDefaultValue
136
276
  }
137
277
  // If a field is required, return the default value (will be validated later)
@@ -145,18 +285,18 @@ export function fax<IsRequired extends boolean = true>(options?: FaxOptions<IsRe
145
285
  return processed
146
286
  }
147
287
 
148
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
288
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
149
289
 
150
290
  const schema = baseSchema.refine((val) => {
151
291
  if (val === null) return true
152
292
 
153
293
  // Required check
154
- if (required && (val === "" || val === "null" || val === "undefined")) {
294
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
155
295
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
156
296
  }
157
297
 
158
298
  if (val === null) return true
159
- if (!required && val === "") return true
299
+ if (!isRequired && val === "") return true
160
300
 
161
301
  // Allowlist check (if an allowlist is provided, only allow values in the allowlist)
162
302
  if (whitelist && whitelist.length > 0) {
@@ -178,5 +318,19 @@ export function fax<IsRequired extends boolean = true>(options?: FaxOptions<IsRe
178
318
  return schema as unknown as FaxSchema<IsRequired>
179
319
  }
180
320
 
181
- // Export utility function for external use
321
+ /**
322
+ * Utility function exported for external use
323
+ *
324
+ * @description
325
+ * The validation function can be used independently for fax number validation
326
+ * without creating a full Zod schema.
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * import { validateTaiwanFax } from './fax'
331
+ *
332
+ * // Direct validation
333
+ * const isValid = validateTaiwanFax("0223456789") // boolean
334
+ * ```
335
+ */
182
336
  export { validateTaiwanFax }