@hy_ong/zod-kit 0.0.2 → 0.0.5
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 +23 -0
- package/LICENSE +21 -0
- package/README.md +7 -7
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +1663 -189
- package/dist/index.d.cts +324 -32
- package/dist/index.d.ts +324 -32
- package/dist/index.js +1634 -187
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +123 -49
- package/src/i18n/locales/zh-TW.json +123 -46
- package/src/index.ts +13 -7
- package/src/validators/common/boolean.ts +97 -0
- package/src/validators/common/date.ts +171 -0
- package/src/validators/common/email.ts +200 -0
- package/src/validators/common/id.ts +259 -0
- package/src/validators/common/number.ts +194 -0
- package/src/validators/common/password.ts +214 -0
- package/src/validators/common/text.ts +151 -0
- package/src/validators/common/url.ts +207 -0
- package/src/validators/taiwan/business-id.ts +140 -0
- package/src/validators/taiwan/fax.ts +182 -0
- package/src/validators/taiwan/mobile.ts +110 -0
- package/src/validators/taiwan/national-id.ts +208 -0
- package/src/validators/taiwan/tel.ts +182 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +281 -54
- package/tests/common/text.test.ts +227 -30
- 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/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -37
- package/src/common/date.ts +0 -44
- package/src/common/email.ts +0 -45
- package/src/common/integer.ts +0 -47
- package/src/common/number.ts +0 -38
- package/src/common/password.ts +0 -34
- package/src/common/text.ts +0 -35
- package/src/common/url.ts +0 -38
- package/tests/common/integer.test.ts +0 -90
|
@@ -5,25 +5,39 @@ const locales = [
|
|
|
5
5
|
{
|
|
6
6
|
locale: "en",
|
|
7
7
|
messages: {
|
|
8
|
-
required: "
|
|
9
|
-
min: "
|
|
10
|
-
max: "
|
|
11
|
-
uppercase: "
|
|
12
|
-
lowercase: "
|
|
13
|
-
digits: "
|
|
14
|
-
special: "
|
|
8
|
+
required: "Required",
|
|
9
|
+
min: "Must be at least 8 characters",
|
|
10
|
+
max: "Must be at most 20 characters",
|
|
11
|
+
uppercase: "Must include at least one uppercase letter",
|
|
12
|
+
lowercase: "Must include at least one lowercase letter",
|
|
13
|
+
digits: "Must include at least one digit",
|
|
14
|
+
special: "Must include at least one special character",
|
|
15
|
+
noRepeating: "Must not contain repeating characters",
|
|
16
|
+
noSequential: "Must not contain sequential characters",
|
|
17
|
+
noCommonWords: "Must not contain common words or patterns",
|
|
18
|
+
minStrength: "Password strength must be at least strong",
|
|
19
|
+
excludes: "Must not contain test",
|
|
20
|
+
includes: "Must include @",
|
|
21
|
+
invalid: "Invalid password format",
|
|
15
22
|
},
|
|
16
23
|
},
|
|
17
24
|
{
|
|
18
25
|
locale: "zh-TW",
|
|
19
26
|
messages: {
|
|
20
|
-
required: "
|
|
21
|
-
min: "
|
|
22
|
-
max: "
|
|
23
|
-
uppercase: "
|
|
24
|
-
lowercase: "
|
|
25
|
-
digits: "
|
|
26
|
-
special: "
|
|
27
|
+
required: "必填",
|
|
28
|
+
min: "長度至少 8 字元",
|
|
29
|
+
max: "長度最多 20 字元",
|
|
30
|
+
uppercase: "必須包含至少一個大寫字母",
|
|
31
|
+
lowercase: "必須包含至少一個小寫字母",
|
|
32
|
+
digits: "必須包含至少一個數字",
|
|
33
|
+
special: "必須包含至少一個特殊符號",
|
|
34
|
+
noRepeating: "不可包含重複字元",
|
|
35
|
+
noSequential: "不可包含連續字元",
|
|
36
|
+
noCommonWords: "不可包含常見密碼或模式",
|
|
37
|
+
minStrength: "密碼強度必須至少為 strong",
|
|
38
|
+
excludes: "不得包含「test」",
|
|
39
|
+
includes: "必須包含「@」",
|
|
40
|
+
invalid: "密碼格式錯誤",
|
|
27
41
|
},
|
|
28
42
|
},
|
|
29
43
|
] as const
|
|
@@ -31,59 +45,272 @@ const locales = [
|
|
|
31
45
|
describe.each(locales)("password() locale: $locale", ({ locale, messages }) => {
|
|
32
46
|
beforeEach(() => setLocale(locale as Locale))
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
describe("basic functionality", () => {
|
|
49
|
+
it("should pass with valid string", () => {
|
|
50
|
+
const schema = password()
|
|
51
|
+
expect(schema.parse("hello")).toBe("hello")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("should fail with empty string when required", () => {
|
|
55
|
+
const schema = password()
|
|
56
|
+
expect(() => schema.parse("")).toThrow(messages.required)
|
|
57
|
+
expect(() => schema.parse(null)).toThrow(messages.required)
|
|
58
|
+
expect(() => schema.parse(undefined)).toThrow(messages.required)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("should pass with null when not required", () => {
|
|
62
|
+
const schema = password({ required: false })
|
|
63
|
+
expect(schema.parse("")).toBe(null)
|
|
64
|
+
expect(schema.parse(null)).toBe(null)
|
|
65
|
+
expect(schema.parse(undefined)).toBe(null)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should handle default values", () => {
|
|
69
|
+
const schema = password({ required: false, defaultValue: "default123" })
|
|
70
|
+
expect(schema.parse("")).toBe("default123")
|
|
71
|
+
expect(schema.parse(null)).toBe("default123")
|
|
72
|
+
expect(schema.parse(undefined)).toBe("default123")
|
|
73
|
+
expect(schema.parse("actual")).toBe("actual")
|
|
74
|
+
})
|
|
38
75
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
expect(() => schema.parse(undefined)).toThrow(messages.required)
|
|
76
|
+
it("should apply transform function", () => {
|
|
77
|
+
const schema = password({ transform: (val) => val.toLowerCase() })
|
|
78
|
+
expect(schema.parse("HELLO")).toBe("hello")
|
|
79
|
+
})
|
|
44
80
|
})
|
|
45
81
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
describe("length validation", () => {
|
|
83
|
+
it("should fail with string shorter than min", () => {
|
|
84
|
+
const schema = password({ min: 8 })
|
|
85
|
+
expect(() => schema.parse("short")).toThrow(messages.min)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("should fail with string longer than max", () => {
|
|
89
|
+
const schema = password({ max: 20 })
|
|
90
|
+
expect(() => schema.parse("this_is_a_very_long_password_that_exceeds_limit")).toThrow(messages.max)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("should pass with valid length", () => {
|
|
94
|
+
const schema = password({ min: 5, max: 15 })
|
|
95
|
+
expect(schema.parse("validpassword")).toBe("validpassword")
|
|
96
|
+
})
|
|
51
97
|
})
|
|
52
98
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
99
|
+
describe("character requirements", () => {
|
|
100
|
+
it("should enforce uppercase requirement", () => {
|
|
101
|
+
const schema = password({ uppercase: true })
|
|
102
|
+
expect(() => schema.parse("lowercase1!")).toThrow(messages.uppercase)
|
|
103
|
+
expect(schema.parse("Hello1!")).toBe("Hello1!")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("should enforce lowercase requirement", () => {
|
|
107
|
+
const schema = password({ lowercase: true })
|
|
108
|
+
expect(() => schema.parse("UPPERCASE1!")).toThrow(messages.lowercase)
|
|
109
|
+
expect(schema.parse("Test1!")).toBe("Test1!")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("should enforce digit requirement", () => {
|
|
113
|
+
const schema = password({ digits: true })
|
|
114
|
+
expect(() => schema.parse("NoDigits!")).toThrow(messages.digits)
|
|
115
|
+
expect(schema.parse("With1!")).toBe("With1!")
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("should enforce special character requirement", () => {
|
|
119
|
+
const schema = password({ special: true })
|
|
120
|
+
expect(() => schema.parse("NoSpecial1")).toThrow(messages.special)
|
|
121
|
+
expect(schema.parse("Valid1!")).toBe("Valid1!")
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("should enforce multiple requirements", () => {
|
|
125
|
+
const schema = password({ uppercase: true, lowercase: true, digits: true, special: true })
|
|
126
|
+
expect(() => schema.parse("nouppercase1!")).toThrow(messages.uppercase)
|
|
127
|
+
expect(() => schema.parse("NOLOWERCASE1!")).toThrow(messages.lowercase)
|
|
128
|
+
expect(() => schema.parse("NoDigits!")).toThrow(messages.digits)
|
|
129
|
+
expect(() => schema.parse("NoSpecial1")).toThrow(messages.special)
|
|
130
|
+
expect(schema.parse("Valid1!")).toBe("Valid1!")
|
|
131
|
+
})
|
|
59
132
|
})
|
|
60
133
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
134
|
+
describe("advanced security features", () => {
|
|
135
|
+
it("should enforce no repeating characters", () => {
|
|
136
|
+
const schema = password({ noRepeating: true })
|
|
137
|
+
expect(() => schema.parse("password111")).toThrow(messages.noRepeating)
|
|
138
|
+
expect(() => schema.parse("helllo")).toThrow(messages.noRepeating)
|
|
139
|
+
expect(schema.parse("password")).toBe("password")
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it("should enforce no sequential characters", () => {
|
|
143
|
+
const schema = password({ noSequential: true })
|
|
144
|
+
expect(() => schema.parse("abc123")).toThrow(messages.noSequential)
|
|
145
|
+
expect(() => schema.parse("xyz789")).toThrow(messages.noSequential)
|
|
146
|
+
expect(() => schema.parse("password456")).toThrow(messages.noSequential)
|
|
147
|
+
expect(schema.parse("random135")).toBe("random135")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it("should enforce no common words", () => {
|
|
151
|
+
const schema = password({ noCommonWords: true })
|
|
152
|
+
expect(() => schema.parse("password123")).toThrow(messages.noCommonWords)
|
|
153
|
+
expect(() => schema.parse("admin")).toThrow(messages.noCommonWords)
|
|
154
|
+
expect(() => schema.parse("qwerty")).toThrow(messages.noCommonWords)
|
|
155
|
+
expect(schema.parse("randomtext")).toBe("randomtext")
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("should enforce minimum strength", () => {
|
|
159
|
+
const schema = password({ minStrength: "strong" })
|
|
160
|
+
expect(() => schema.parse("weak")).toThrow(messages.minStrength)
|
|
161
|
+
expect(() => schema.parse("123456")).toThrow(messages.minStrength)
|
|
162
|
+
expect(schema.parse("StrongP@ssw0rd")).toBe("StrongP@ssw0rd")
|
|
163
|
+
})
|
|
64
164
|
})
|
|
65
165
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
166
|
+
describe("content validation", () => {
|
|
167
|
+
it("should enforce includes requirement", () => {
|
|
168
|
+
const schema = password({ includes: "@" })
|
|
169
|
+
expect(() => schema.parse("password")).toThrow(messages.includes)
|
|
170
|
+
expect(schema.parse("user@pass")).toBe("user@pass")
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("should enforce excludes requirement with string", () => {
|
|
174
|
+
const schema = password({ excludes: "test" })
|
|
175
|
+
expect(() => schema.parse("testpassword")).toThrow(messages.excludes)
|
|
176
|
+
expect(schema.parse("password")).toBe("password")
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it("should enforce excludes requirement with array", () => {
|
|
180
|
+
const schema = password({ excludes: ["test", "admin"] })
|
|
181
|
+
expect(() => schema.parse("testpassword")).toThrow()
|
|
182
|
+
expect(() => schema.parse("adminpass")).toThrow()
|
|
183
|
+
expect(schema.parse("password")).toBe("password")
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should enforce regex requirement", () => {
|
|
187
|
+
const schema = password({ regex: /^[a-zA-Z0-9!@#$%^&*()]+$/ })
|
|
188
|
+
expect(() => schema.parse("invalid-password")).toThrow(messages.invalid)
|
|
189
|
+
expect(schema.parse("Valid123!")).toBe("Valid123!")
|
|
190
|
+
})
|
|
70
191
|
})
|
|
71
192
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
193
|
+
describe("custom i18n messages", () => {
|
|
194
|
+
it("should use custom i18n messages when provided", () => {
|
|
195
|
+
const customMessages = {
|
|
196
|
+
en: {
|
|
197
|
+
required: "Custom required message",
|
|
198
|
+
min: "Custom min ${min} message",
|
|
199
|
+
uppercase: "Custom uppercase message",
|
|
200
|
+
},
|
|
201
|
+
"zh-TW": {
|
|
202
|
+
required: "自訂必填訊息",
|
|
203
|
+
min: "自訂最小長度 ${min} 訊息",
|
|
204
|
+
uppercase: "自訂大寫字母訊息",
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const schema = password({
|
|
209
|
+
min: 8,
|
|
210
|
+
uppercase: true,
|
|
211
|
+
i18n: customMessages,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (locale === "en") {
|
|
215
|
+
expect(() => schema.parse("")).toThrow("Custom required message")
|
|
216
|
+
expect(() => schema.parse("short")).toThrow("Custom min 8 message")
|
|
217
|
+
expect(() => schema.parse("nouppercase")).toThrow("Custom uppercase message")
|
|
218
|
+
} else {
|
|
219
|
+
expect(() => schema.parse("")).toThrow("自訂必填訊息")
|
|
220
|
+
expect(() => schema.parse("short")).toThrow("自訂最小長度 8 訊息")
|
|
221
|
+
expect(() => schema.parse("nouppercase")).toThrow("自訂大寫字母訊息")
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it("should fallback to default i18n when custom message not provided", () => {
|
|
226
|
+
const customMessages = {
|
|
227
|
+
en: { required: "Custom required message" },
|
|
228
|
+
"zh-TW": { required: "自訂必填訊息" },
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const schema = password({
|
|
232
|
+
uppercase: true,
|
|
233
|
+
i18n: customMessages,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
if (locale === "en") {
|
|
237
|
+
expect(() => schema.parse("")).toThrow("Custom required message")
|
|
238
|
+
expect(() => schema.parse("nouppercase")).toThrow(messages.uppercase)
|
|
239
|
+
} else {
|
|
240
|
+
expect(() => schema.parse("")).toThrow("自訂必填訊息")
|
|
241
|
+
expect(() => schema.parse("nouppercase")).toThrow(messages.uppercase)
|
|
242
|
+
}
|
|
243
|
+
})
|
|
76
244
|
})
|
|
77
245
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
246
|
+
describe("complex scenarios", () => {
|
|
247
|
+
it("should handle all features combined", () => {
|
|
248
|
+
const schema = password({
|
|
249
|
+
min: 12,
|
|
250
|
+
max: 50,
|
|
251
|
+
uppercase: true,
|
|
252
|
+
lowercase: true,
|
|
253
|
+
digits: true,
|
|
254
|
+
special: true,
|
|
255
|
+
noRepeating: true,
|
|
256
|
+
noSequential: true,
|
|
257
|
+
noCommonWords: true,
|
|
258
|
+
minStrength: "strong",
|
|
259
|
+
includes: "@",
|
|
260
|
+
excludes: ["test", "admin"],
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Should fail for various reasons
|
|
264
|
+
expect(() => schema.parse("short")).toThrow()
|
|
265
|
+
expect(() => schema.parse("NoLowercase123!@")).toThrow()
|
|
266
|
+
expect(() => schema.parse("nouppercase123!@")).toThrow()
|
|
267
|
+
expect(() => schema.parse("NoDigits!@")).toThrow()
|
|
268
|
+
expect(() => schema.parse("NoSpecial123@")).toThrow()
|
|
269
|
+
expect(() => schema.parse("Repeating111!@")).toThrow()
|
|
270
|
+
expect(() => schema.parse("Sequential123!@")).toThrow()
|
|
271
|
+
expect(() => schema.parse("Password123!@")).toThrow()
|
|
272
|
+
expect(() => schema.parse("Complex123!")).toThrow() // Missing @
|
|
273
|
+
expect(() => schema.parse("TestComplex123!@")).toThrow() // Contains "test"
|
|
274
|
+
|
|
275
|
+
// Should pass
|
|
276
|
+
expect(schema.parse("C0mpl3x@P@ssw0rd")).toBe("C0mpl3x@P@ssw0rd")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("should work with optional password and all features", () => {
|
|
280
|
+
const schema = password({
|
|
281
|
+
required: false,
|
|
282
|
+
min: 8,
|
|
283
|
+
uppercase: true,
|
|
284
|
+
lowercase: true,
|
|
285
|
+
digits: true,
|
|
286
|
+
special: true,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
expect(schema.parse(null)).toBe(null)
|
|
290
|
+
expect(schema.parse("")).toBe(null)
|
|
291
|
+
expect(() => schema.parse("weak")).toThrow()
|
|
292
|
+
expect(schema.parse("Strong1!")).toBe("Strong1!")
|
|
293
|
+
})
|
|
82
294
|
})
|
|
83
295
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
296
|
+
describe("password strength calculation", () => {
|
|
297
|
+
const testStrengthScenarios = [
|
|
298
|
+
{ password: "weak", expectedToPass: false, minStrength: "medium" as const },
|
|
299
|
+
{ password: "Medium1", expectedToPass: false, minStrength: "strong" as const },
|
|
300
|
+
{ password: "Strong1!", expectedToPass: true, minStrength: "medium" as const },
|
|
301
|
+
{ password: "VeryStrongP@ssw0rd123", expectedToPass: true, minStrength: "strong" as const },
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
testStrengthScenarios.forEach(({ password: testPassword, expectedToPass, minStrength }) => {
|
|
305
|
+
it(`should ${expectedToPass ? "pass" : "fail"} for password "${testPassword}" with minStrength "${minStrength}"`, () => {
|
|
306
|
+
const schema = password({ minStrength })
|
|
307
|
+
|
|
308
|
+
if (expectedToPass) {
|
|
309
|
+
expect(schema.parse(testPassword)).toBe(testPassword)
|
|
310
|
+
} else {
|
|
311
|
+
expect(() => schema.parse(testPassword)).toThrow()
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
})
|
|
88
315
|
})
|
|
89
|
-
})
|
|
316
|
+
})
|