@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.
- package/.claude/settings.local.json +9 -1
- package/README.md +465 -97
- package/dist/index.cjs +1690 -179
- package/dist/index.d.cts +2791 -28
- package/dist/index.d.ts +2791 -28
- package/dist/index.js +1672 -178
- package/package.json +2 -1
- package/src/i18n/locales/en.json +62 -0
- package/src/i18n/locales/zh-TW.json +62 -0
- package/src/index.ts +4 -0
- package/src/validators/common/boolean.ts +101 -4
- package/src/validators/common/date.ts +141 -6
- package/src/validators/common/datetime.ts +680 -0
- package/src/validators/common/email.ts +120 -4
- package/src/validators/common/file.ts +391 -0
- package/src/validators/common/id.ts +230 -18
- package/src/validators/common/number.ts +132 -4
- package/src/validators/common/password.ts +187 -8
- package/src/validators/common/text.ts +130 -6
- package/src/validators/common/time.ts +607 -0
- package/src/validators/common/url.ts +153 -6
- package/src/validators/taiwan/business-id.ts +138 -9
- package/src/validators/taiwan/fax.ts +164 -10
- package/src/validators/taiwan/mobile.ts +151 -10
- package/src/validators/taiwan/national-id.ts +233 -17
- package/src/validators/taiwan/postal-code.ts +1048 -0
- package/src/validators/taiwan/tel.ts +167 -10
- package/tests/common/boolean.test.ts +38 -38
- package/tests/common/date.test.ts +65 -65
- package/tests/common/datetime.test.ts +675 -0
- package/tests/common/email.test.ts +24 -28
- package/tests/common/file.test.ts +475 -0
- package/tests/common/id.test.ts +80 -113
- package/tests/common/number.test.ts +24 -25
- package/tests/common/password.test.ts +28 -35
- package/tests/common/text.test.ts +36 -37
- package/tests/common/time.test.ts +510 -0
- package/tests/common/url.test.ts +67 -67
- package/tests/taiwan/business-id.test.ts +22 -22
- package/tests/taiwan/fax.test.ts +33 -42
- package/tests/taiwan/mobile.test.ts +32 -41
- package/tests/taiwan/national-id.test.ts +31 -31
- package/tests/taiwan/postal-code.test.ts +751 -0
- package/tests/taiwan/tel.test.ts +33 -42
- package/debug.js +0 -21
- 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
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? (
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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 ?? (
|
|
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 (!
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
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 }
|