@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
@@ -1,77 +1,249 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest"
2
- import { setLocale, email, Locale } from "../../src"
3
-
4
- const locales = [
5
- {
6
- locale: "en",
7
- messages: {
8
- required: "Required",
9
- min: "Must be at least 20 characters",
10
- max: "Must be at most 10 characters",
11
- includes: "Must include company",
12
- invalid: "Invalid format",
13
- domain: "Must be under the domain @example.com",
14
- },
15
- },
16
- {
17
- locale: "zh-TW",
18
- messages: {
19
- required: "必填",
20
- min: "長度至少 20 字元",
21
- max: "長度最多 10 字元",
22
- includes: "必須包含「company」",
23
- invalid: "格式錯誤",
24
- domain: "必須為 @example.com 網域",
25
- },
26
- },
27
- ] as const
28
-
29
- describe.each(locales)("email() locale: $locale", ({ locale, messages }) => {
30
- beforeEach(() => setLocale(locale as Locale))
31
-
32
- it("should pass for valid email", () => {
33
- const schema = email()
34
- expect(schema.parse("test@example.com")).toBe("test@example.com")
2
+ import { email, setLocale } from "../../src"
3
+
4
+ describe("email() features", () => {
5
+ beforeEach(() => setLocale("en"))
6
+
7
+ describe("multiple domain support", () => {
8
+ it("should accept multiple allowed domains", () => {
9
+ const schema = email({ domain: ["example.com", "company.org"] })
10
+ expect(schema.parse("test@example.com")).toBe("test@example.com")
11
+ expect(schema.parse("user@company.org")).toBe("user@company.org")
12
+ })
13
+
14
+ it("should reject domains not in whitelist", () => {
15
+ const schema = email({ domain: ["example.com", "company.org"] })
16
+ expect(() => schema.parse("test@notallowed.com")).toThrow("Must be from domain: example.com, company.org")
17
+ })
35
18
  })
36
19
 
37
- it("should fail for invalid email format", () => {
38
- const schema = email()
39
- expect(() => schema.parse("invalid-email")).toThrow(messages.invalid)
20
+ describe("subdomain support", () => {
21
+ it("should allow subdomains by default", () => {
22
+ const schema = email({ domain: "example.com" })
23
+ expect(schema.parse("test@mail.example.com")).toBe("test@mail.example.com")
24
+ expect(schema.parse("user@api.example.com")).toBe("user@api.example.com")
25
+ })
26
+
27
+ it("should reject subdomains when allowSubdomains=false", () => {
28
+ const schema = email({ domain: "example.com", allowSubdomains: false })
29
+ expect(schema.parse("test@example.com")).toBe("test@example.com")
30
+ expect(() => schema.parse("test@mail.example.com")).toThrow("Must be from domain: example.com")
31
+ })
32
+ })
33
+
34
+ describe("domain blacklist", () => {
35
+ it("should reject blacklisted domains", () => {
36
+ const schema = email({ domainBlacklist: ["spam.com", "fake.org"] })
37
+ expect(schema.parse("test@example.com")).toBe("test@example.com")
38
+ expect(() => schema.parse("test@spam.com")).toThrow("Domain spam.com is not allowed")
39
+ expect(() => schema.parse("user@fake.org")).toThrow("Domain fake.org is not allowed")
40
+ })
41
+
42
+ it("should reject blacklisted subdomains when allowSubdomains=true", () => {
43
+ const schema = email({ domainBlacklist: ["spam.com"], allowSubdomains: true })
44
+ expect(() => schema.parse("test@mail.spam.com")).toThrow("Domain mail.spam.com is not allowed")
45
+ })
40
46
  })
41
47
 
42
- it("should enforce required when set", () => {
43
- const schema = email({ required: true })
44
- expect(() => schema.parse("")).toThrow(messages.required)
45
- expect(() => schema.parse(null)).toThrow(messages.required)
46
- expect(() => schema.parse(undefined)).toThrow(messages.required)
48
+ describe("business email validation", () => {
49
+ it("should reject free email providers when businessOnly=true", () => {
50
+ const schema = email({ businessOnly: true })
51
+ expect(schema.parse("john@company.com")).toBe("john@company.com")
52
+ expect(() => schema.parse("john@gmail.com")).toThrow("Only business email addresses are allowed")
53
+ expect(() => schema.parse("jane@yahoo.com")).toThrow("Only business email addresses are allowed")
54
+ expect(() => schema.parse("test@hotmail.com")).toThrow("Only business email addresses are allowed")
55
+ })
47
56
  })
48
57
 
49
- it("should allow null if not required", () => {
50
- const schema = email({ required: false })
51
- expect(schema.parse("")).toBe(null)
52
- expect(schema.parse(null)).toBe(null)
53
- expect(schema.parse(undefined)).toBe(null)
58
+ describe("disposable email detection", () => {
59
+ it("should reject disposable email addresses when noDisposable=true", () => {
60
+ const schema = email({ noDisposable: true })
61
+ expect(schema.parse("john@company.com")).toBe("john@company.com")
62
+ expect(() => schema.parse("test@10minutemail.com")).toThrow("Disposable email addresses are not allowed")
63
+ expect(() => schema.parse("user@tempmail.org")).toThrow("Disposable email addresses are not allowed")
64
+ expect(() => schema.parse("temp@mailinator.com")).toThrow("Disposable email addresses are not allowed")
65
+ })
66
+ })
67
+
68
+ describe("case handling", () => {
69
+ it("should convert to lowercase by default", () => {
70
+ const schema = email()
71
+ expect(schema.parse("Test@EXAMPLE.COM")).toBe("test@example.com")
72
+ expect(schema.parse("USER@Company.Org")).toBe("user@company.org")
73
+ })
74
+
75
+ it("should preserve case when lowercase=false", () => {
76
+ const schema = email({ lowercase: false })
77
+ expect(schema.parse("Test@EXAMPLE.COM")).toBe("Test@EXAMPLE.COM")
78
+ expect(schema.parse("USER@Company.Org")).toBe("USER@Company.Org")
79
+ })
80
+ })
81
+
82
+ describe("excludes functionality", () => {
83
+ it("should reject emails containing excluded strings", () => {
84
+ const schema = email({ excludes: ["test", "demo"] })
85
+ expect(schema.parse("john@example.com")).toBe("john@example.com")
86
+ expect(() => schema.parse("test@example.com")).toThrow("Must include test")
87
+ expect(() => schema.parse("demo.user@example.com")).toThrow("Must include demo")
88
+ })
89
+
90
+ it("should handle excludes as array", () => {
91
+ const schema = email({ excludes: ["spam", "fake", "test"] })
92
+ expect(() => schema.parse("spam@example.com")).toThrow("Must include spam")
93
+ expect(() => schema.parse("fake.user@example.com")).toThrow("Must include fake")
94
+ expect(() => schema.parse("user@test.com")).toThrow("Must include test")
95
+ })
54
96
  })
55
97
 
56
- it("should enforce max length", () => {
57
- const schema = email({ max: 10 })
58
- expect(() => schema.parse("verylongemail@example.com")).toThrow(messages.max)
98
+ describe("transform functionality", () => {
99
+ it("should apply transform function", () => {
100
+ const schema = email({
101
+ transform: (val) => val.replace(/test/g, "user"), // Replace test with user
102
+ })
103
+ expect(schema.parse("test@example.com")).toBe("user@example.com")
104
+ expect(schema.parse("test.email@company.org")).toBe("user.email@company.org")
105
+ })
106
+
107
+ it("should apply transform after lowercase", () => {
108
+ const schema = email({
109
+ lowercase: true,
110
+ transform: (val) => val.replace(/\+[^@]*/, ""), // Remove + aliases
111
+ })
112
+ expect(schema.parse("USER+tag@EXAMPLE.COM")).toBe("user@example.com")
113
+ })
59
114
  })
60
115
 
61
- it("should enforce min length", () => {
62
- const schema = email({ min: 20 })
63
- expect(() => schema.parse("short@example.com")).toThrow(messages.min)
116
+ describe("default value functionality", () => {
117
+ it("should use defaultValue for empty input when required", () => {
118
+ const schema = email({ defaultValue: "default@example.com" })
119
+ expect(schema.parse("")).toBe("default@example.com")
120
+ expect(schema.parse(null)).toBe("default@example.com")
121
+ expect(schema.parse(undefined)).toBe("default@example.com")
122
+ })
123
+
124
+ it("should use defaultValue for empty input when not required", () => {
125
+ const schema = email({ required: false, defaultValue: "default@example.com" })
126
+ expect(schema.parse("")).toBe("default@example.com")
127
+ expect(schema.parse(null)).toBe("default@example.com")
128
+ expect(schema.parse(undefined)).toBe("default@example.com")
129
+ })
64
130
  })
65
131
 
66
- it("should enforce includes", () => {
67
- const schema = email({ includes: "company" })
68
- expect(schema.parse("john@company.com")).toBe("john@company.com")
69
- expect(() => schema.parse("short@other.com")).toThrow(messages.includes)
132
+ describe("custom i18n messages", () => {
133
+ it("should use custom messages when provided", () => {
134
+ const schema = email({
135
+ domain: "company.com",
136
+ businessOnly: true,
137
+ i18n: {
138
+ en: {
139
+ required: "Custom required message",
140
+ domain: "Custom domain message: ${domain}",
141
+ businessOnly: "Custom business only message",
142
+ },
143
+ "zh-TW": {
144
+ required: "客製化必填訊息",
145
+ domain: "客製化網域訊息: ${domain}",
146
+ businessOnly: "客製化僅限企業郵箱訊息",
147
+ },
148
+ },
149
+ })
150
+
151
+ expect(() => schema.parse("")).toThrow("Custom required message")
152
+ expect(() => schema.parse("test@example.com")).toThrow("Custom domain message: company.com")
153
+ expect(() => schema.parse("test@gmail.com")).toThrow("Custom business only message")
154
+ })
155
+
156
+ it("should fallback to default messages when custom not provided", () => {
157
+ const schema = email({
158
+ noDisposable: true,
159
+ i18n: {
160
+ en: {
161
+ required: "Custom required message",
162
+ },
163
+ "zh-TW": {
164
+ required: "客製化必填訊息",
165
+ },
166
+ },
167
+ })
168
+
169
+ expect(() => schema.parse("")).toThrow("Custom required message")
170
+ expect(() => schema.parse("test@tempmail.org")).toThrow("Disposable email addresses are not allowed")
171
+ })
172
+
173
+ it("should use correct locale for custom messages", () => {
174
+ setLocale("en")
175
+ const schemaEn = email({
176
+ businessOnly: true,
177
+ i18n: {
178
+ en: {
179
+ businessOnly: "English business message",
180
+ },
181
+ "zh-TW": {
182
+ businessOnly: "繁體中文企業訊息",
183
+ },
184
+ },
185
+ })
186
+ expect(() => schemaEn.parse("test@gmail.com")).toThrow("English business message")
187
+
188
+ setLocale("zh-TW")
189
+ const schemaZh = email({
190
+ businessOnly: true,
191
+ i18n: {
192
+ en: {
193
+ businessOnly: "English business message",
194
+ },
195
+ "zh-TW": {
196
+ businessOnly: "繁體中文企業訊息",
197
+ },
198
+ },
199
+ })
200
+ expect(() => schemaZh.parse("test@gmail.com")).toThrow("繁體中文企業訊息")
201
+ })
70
202
  })
71
203
 
72
- it("should validate against custom domain", () => {
73
- const schema = email({ domain: "example.com" })
74
- expect(schema.parse("test@example.com")).toBe("test@example.com")
75
- expect(() => schema.parse("short@notexample.com")).toThrow(messages.domain)
204
+ describe("complex scenarios", () => {
205
+ it("should work with multiple validations", () => {
206
+ const schema = email({
207
+ domain: ["company.com", "business.org"],
208
+ minLength: 10,
209
+ maxLength: 30,
210
+ businessOnly: true,
211
+ noDisposable: true,
212
+ allowSubdomains: false,
213
+ })
214
+
215
+ expect(schema.parse("john@company.com")).toBe("john@company.com")
216
+ expect(schema.parse("jane@business.org")).toBe("jane@business.org")
217
+
218
+ expect(() => schema.parse("a@co.me")).toThrow("Must be at least 10 characters")
219
+ expect(() => schema.parse("verylongusernamethatexceedslimit@company.com")).toThrow("Must be at most 30 characters")
220
+ expect(() => schema.parse("test@mail.company.com")).toThrow("Must be from domain: company.com, business.org")
221
+ expect(() => schema.parse("user@gmail.com")).toThrow("Only business email addresses are allowed")
222
+ })
223
+
224
+ it("should handle null values correctly when not required", () => {
225
+ const schema = email({
226
+ required: false,
227
+ domain: "example.com",
228
+ minLength: 10,
229
+ })
230
+
231
+ expect(schema.parse(null)).toBe(null)
232
+ expect(schema.parse("")).toBe(null)
233
+ expect(schema.parse(undefined)).toBe(null)
234
+ expect(schema.parse("longuser@example.com")).toBe("longuser@example.com")
235
+ })
236
+
237
+ it("should work with transform, lowercase, and domain validation together", () => {
238
+ const schema = email({
239
+ domain: "company.com",
240
+ lowercase: true,
241
+ transform: (val) => val.replace(/\+[^@]*/, ""), // Remove + aliases
242
+ allowSubdomains: true,
243
+ })
244
+
245
+ expect(schema.parse("USER+tag@MAIL.COMPANY.COM")).toBe("user@mail.company.com")
246
+ expect(schema.parse("TEST+alias@COMPANY.COM")).toBe("test@company.com")
247
+ })
76
248
  })
77
249
  })