@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,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
+ }