@hy_ong/zod-kit 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Text validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive text validation with length constraints, content validation,
|
|
5
|
+
* flexible trimming and casing options, and advanced transformation features.
|
|
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 text validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface TextMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [notEmpty] - Message when field must not be empty (whitespace-only)
|
|
21
|
+
* @property {string} [minLength] - Message when text is shorter than minimum length
|
|
22
|
+
* @property {string} [maxLength] - Message when text exceeds maximum length
|
|
23
|
+
* @property {string} [startsWith] - Message when text doesn't start with required string
|
|
24
|
+
* @property {string} [endsWith] - Message when text doesn't end with required string
|
|
25
|
+
* @property {string} [includes] - Message when text doesn't contain required string
|
|
26
|
+
* @property {string} [excludes] - Message when text contains forbidden string
|
|
27
|
+
* @property {string} [invalid] - Message when text doesn't match regex pattern
|
|
28
|
+
*/
|
|
29
|
+
export type TextMessages = {
|
|
30
|
+
required?: string
|
|
31
|
+
notEmpty?: string
|
|
32
|
+
minLength?: string
|
|
33
|
+
maxLength?: string
|
|
34
|
+
startsWith?: string
|
|
35
|
+
endsWith?: string
|
|
36
|
+
includes?: string
|
|
37
|
+
excludes?: string
|
|
38
|
+
invalid?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Configuration options for text validation
|
|
43
|
+
*
|
|
44
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
45
|
+
*
|
|
46
|
+
* @interface TextOptions
|
|
47
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
48
|
+
* @property {number} [minLength] - Minimum length of text
|
|
49
|
+
* @property {number} [maxLength] - Maximum length of text
|
|
50
|
+
* @property {string} [startsWith] - String that text must start with
|
|
51
|
+
* @property {string} [endsWith] - String that text must end with
|
|
52
|
+
* @property {string} [includes] - String that must be included in text
|
|
53
|
+
* @property {string | string[]} [excludes] - String(s) that must not be included
|
|
54
|
+
* @property {RegExp} [regex] - Regular expression pattern for validation
|
|
55
|
+
* @property {"trim" | "trimStart" | "trimEnd" | "none"} [trimMode="trim"] - Whitespace handling
|
|
56
|
+
* @property {"upper" | "lower" | "title" | "none"} [casing="none"] - Case transformation
|
|
57
|
+
* @property {Function} [transform] - Custom transformation function for text
|
|
58
|
+
* @property {boolean} [notEmpty] - Whether to reject whitespace-only strings
|
|
59
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
60
|
+
* @property {Record<Locale, TextMessages>} [i18n] - Custom error messages for different locales
|
|
61
|
+
*/
|
|
62
|
+
export type TextOptions<IsRequired extends boolean = true> = {
|
|
63
|
+
required?: IsRequired
|
|
64
|
+
minLength?: number
|
|
65
|
+
maxLength?: number
|
|
66
|
+
startsWith?: string
|
|
67
|
+
endsWith?: string
|
|
68
|
+
includes?: string
|
|
69
|
+
excludes?: string | string[]
|
|
70
|
+
regex?: RegExp
|
|
71
|
+
trimMode?: "trim" | "trimStart" | "trimEnd" | "none"
|
|
72
|
+
casing?: "upper" | "lower" | "title" | "none"
|
|
73
|
+
transform?: (value: string) => string
|
|
74
|
+
notEmpty?: boolean
|
|
75
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
76
|
+
i18n?: Record<Locale, TextMessages>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Type alias for text validation schema based on required flag
|
|
81
|
+
*
|
|
82
|
+
* @template IsRequired - Whether the field is required
|
|
83
|
+
* @typedef TextSchema
|
|
84
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
85
|
+
*/
|
|
86
|
+
export type TextSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates a Zod schema for text validation with comprehensive string processing
|
|
90
|
+
*
|
|
91
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
92
|
+
* @param {TextOptions<IsRequired>} [options] - Configuration options for text validation
|
|
93
|
+
* @returns {TextSchema<IsRequired>} Zod schema for text validation
|
|
94
|
+
*
|
|
95
|
+
* @description
|
|
96
|
+
* Creates a comprehensive text validator with length constraints, content validation,
|
|
97
|
+
* flexible trimming and casing options, and advanced transformation features.
|
|
98
|
+
*
|
|
99
|
+
* Features:
|
|
100
|
+
* - Length validation (min/max)
|
|
101
|
+
* - Content validation (startsWith, endsWith, includes, excludes)
|
|
102
|
+
* - Regular expression pattern matching
|
|
103
|
+
* - Flexible trimming options (trim, trimStart, trimEnd, none)
|
|
104
|
+
* - Case transformation (upper, lower, title, none)
|
|
105
|
+
* - Empty string vs whitespace-only validation
|
|
106
|
+
* - Custom transformation functions
|
|
107
|
+
* - Comprehensive internationalization
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* // Basic text validation
|
|
112
|
+
* const basicSchema = text()
|
|
113
|
+
* basicSchema.parse("Hello World") // ✓ Valid
|
|
114
|
+
*
|
|
115
|
+
* // Length constraints
|
|
116
|
+
* const lengthSchema = text({ minLength: 3, maxLength: 50 })
|
|
117
|
+
* lengthSchema.parse("Hello") // ✓ Valid
|
|
118
|
+
* lengthSchema.parse("Hi") // ✗ Invalid (too short)
|
|
119
|
+
*
|
|
120
|
+
* // Content validation
|
|
121
|
+
* const contentSchema = text({
|
|
122
|
+
* startsWith: "Hello",
|
|
123
|
+
* endsWith: "!",
|
|
124
|
+
* includes: "World"
|
|
125
|
+
* })
|
|
126
|
+
* contentSchema.parse("Hello World!") // ✓ Valid
|
|
127
|
+
*
|
|
128
|
+
* // Case transformation
|
|
129
|
+
* const upperSchema = text({ casing: "upper" })
|
|
130
|
+
* upperSchema.parse("hello") // ✓ Valid (converted to "HELLO")
|
|
131
|
+
*
|
|
132
|
+
* // Trim modes
|
|
133
|
+
* const trimStartSchema = text({ trimMode: "trimStart" })
|
|
134
|
+
* trimStartSchema.parse(" hello ") // ✓ Valid (result: "hello ")
|
|
135
|
+
*
|
|
136
|
+
* // Regex validation
|
|
137
|
+
* const regexSchema = text({ regex: /^[a-zA-Z]+$/ })
|
|
138
|
+
* regexSchema.parse("hello") // ✓ Valid
|
|
139
|
+
* regexSchema.parse("hello123") // ✗ Invalid
|
|
140
|
+
*
|
|
141
|
+
* // Not empty (rejects whitespace-only)
|
|
142
|
+
* const notEmptySchema = text({ notEmpty: true })
|
|
143
|
+
* notEmptySchema.parse("hello") // ✓ Valid
|
|
144
|
+
* notEmptySchema.parse(" ") // ✗ Invalid
|
|
145
|
+
*
|
|
146
|
+
* // Optional with default
|
|
147
|
+
* const optionalSchema = text({
|
|
148
|
+
* required: false,
|
|
149
|
+
* defaultValue: "default text"
|
|
150
|
+
* })
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
154
|
+
* @see {@link TextOptions} for all available configuration options
|
|
155
|
+
*/
|
|
156
|
+
export function text<IsRequired extends boolean = true>(options?: TextOptions<IsRequired>): TextSchema<IsRequired> {
|
|
157
|
+
const { required = true, minLength, maxLength, startsWith, endsWith, includes, excludes, regex, trimMode = "trim", casing = "none", transform, notEmpty, defaultValue, i18n } = options ?? {}
|
|
158
|
+
|
|
159
|
+
// Set appropriate default value based on required flag
|
|
160
|
+
const actualDefaultValue = defaultValue ?? (required ? "" : null)
|
|
161
|
+
|
|
162
|
+
// Helper function to get custom message or fallback to default i18n
|
|
163
|
+
const getMessage = (key: keyof TextMessages, params?: Record<string, any>) => {
|
|
164
|
+
if (i18n) {
|
|
165
|
+
const currentLocale = getLocale()
|
|
166
|
+
const customMessages = i18n[currentLocale]
|
|
167
|
+
if (customMessages && customMessages[key]) {
|
|
168
|
+
const template = customMessages[key]!
|
|
169
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return t(`common.text.${key}`, params)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Helper functions to apply trimming based on the mode
|
|
176
|
+
const applyTrim = (str: string): string => {
|
|
177
|
+
switch (trimMode) {
|
|
178
|
+
case "trimStart":
|
|
179
|
+
return str.trimStart()
|
|
180
|
+
case "trimEnd":
|
|
181
|
+
return str.trimEnd()
|
|
182
|
+
case "none":
|
|
183
|
+
return str
|
|
184
|
+
default:
|
|
185
|
+
return str.trim()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Helper function to apply casing
|
|
190
|
+
const applyCasing = (str: string): string => {
|
|
191
|
+
switch (casing) {
|
|
192
|
+
case "upper":
|
|
193
|
+
return str.toUpperCase()
|
|
194
|
+
case "lower":
|
|
195
|
+
return str.toLowerCase()
|
|
196
|
+
case "title":
|
|
197
|
+
return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase())
|
|
198
|
+
default:
|
|
199
|
+
return str
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Preprocessing function with optimized transformations
|
|
204
|
+
const preprocessFn = (val: unknown) => {
|
|
205
|
+
if (val === "" || val === null || val === undefined) {
|
|
206
|
+
return actualDefaultValue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let processed = String(val)
|
|
210
|
+
processed = applyTrim(processed)
|
|
211
|
+
processed = applyCasing(processed)
|
|
212
|
+
|
|
213
|
+
if (transform) {
|
|
214
|
+
processed = transform(processed)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return processed
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
221
|
+
|
|
222
|
+
// Single refine with all validations for better performance
|
|
223
|
+
const schema = baseSchema.refine((val) => {
|
|
224
|
+
if (val === null) return true
|
|
225
|
+
|
|
226
|
+
// Required check
|
|
227
|
+
if (required && (val === "" || val === "null" || val === "undefined")) {
|
|
228
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Not empty check (different from required - checks whitespace)
|
|
232
|
+
// For notEmpty, we need to check if the original string (before processing) was only whitespace
|
|
233
|
+
if (notEmpty && val !== null && val.trim() === "") {
|
|
234
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("notEmpty"), path: [] }])
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Length checks
|
|
238
|
+
if (val !== null && minLength !== undefined && val.length < minLength) {
|
|
239
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }])
|
|
240
|
+
}
|
|
241
|
+
if (val !== null && maxLength !== undefined && val.length > maxLength) {
|
|
242
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }])
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// String content checks
|
|
246
|
+
if (val !== null && startsWith !== undefined && !val.startsWith(startsWith)) {
|
|
247
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("startsWith", { startsWith }), path: [] }])
|
|
248
|
+
}
|
|
249
|
+
if (val !== null && endsWith !== undefined && !val.endsWith(endsWith)) {
|
|
250
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("endsWith", { endsWith }), path: [] }])
|
|
251
|
+
}
|
|
252
|
+
if (val !== null && includes !== undefined && !val.includes(includes)) {
|
|
253
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
|
|
254
|
+
}
|
|
255
|
+
if (val !== null && excludes !== undefined) {
|
|
256
|
+
const excludeList = Array.isArray(excludes) ? excludes : [excludes]
|
|
257
|
+
for (const exclude of excludeList) {
|
|
258
|
+
if (val.includes(exclude)) {
|
|
259
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (val !== null && regex !== undefined && !regex.test(val)) {
|
|
264
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid", { regex }), path: [] }])
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return true
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return schema as unknown as TextSchema<IsRequired>
|
|
271
|
+
}
|