zod 4.1.0-canary.20250821T014930 → 4.1.0-canary.20250823T064644

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 (103) hide show
  1. package/package.json +1 -1
  2. package/src/v4/classic/external.ts +0 -1
  3. package/src/v4/classic/parse.ts +49 -0
  4. package/src/v4/classic/schemas.ts +145 -7
  5. package/src/v4/classic/tests/catch.test.ts +25 -0
  6. package/src/v4/classic/tests/codec-examples.test.ts +538 -0
  7. package/src/v4/classic/tests/codec.test.ts +532 -0
  8. package/src/v4/classic/tests/continuability.test.ts +1 -1
  9. package/src/v4/classic/tests/default.test.ts +32 -0
  10. package/src/v4/classic/tests/firstparty.test.ts +4 -0
  11. package/src/v4/classic/tests/function.test.ts +31 -31
  12. package/src/v4/classic/tests/hash.test.ts +68 -0
  13. package/src/v4/classic/tests/nonoptional.test.ts +15 -0
  14. package/src/v4/classic/tests/object.test.ts +31 -0
  15. package/src/v4/classic/tests/pipe.test.ts +25 -5
  16. package/src/v4/classic/tests/prefault.test.ts +25 -0
  17. package/src/v4/classic/tests/preprocess.test.ts +1 -6
  18. package/src/v4/classic/tests/refine.test.ts +76 -3
  19. package/src/v4/classic/tests/string-formats.test.ts +16 -0
  20. package/src/v4/classic/tests/string.test.ts +82 -1
  21. package/src/v4/classic/tests/stringbool.test.ts +40 -0
  22. package/src/v4/classic/tests/template-literal.test.ts +1 -1
  23. package/src/v4/classic/tests/to-json-schema.test.ts +21 -2
  24. package/src/v4/classic/tests/transform.test.ts +7 -0
  25. package/src/v4/classic/tests/union.test.ts +1 -1
  26. package/src/v4/core/api.ts +25 -35
  27. package/src/v4/core/core.ts +7 -26
  28. package/src/v4/core/index.ts +0 -1
  29. package/src/v4/core/json-schema.ts +1 -0
  30. package/src/v4/core/parse.ts +101 -0
  31. package/src/v4/core/regexes.ts +40 -1
  32. package/src/v4/core/schemas.ts +521 -129
  33. package/src/v4/core/to-json-schema.ts +43 -8
  34. package/src/v4/core/util.ts +73 -0
  35. package/src/v4/mini/external.ts +0 -1
  36. package/src/v4/mini/parse.ts +14 -1
  37. package/src/v4/mini/schemas.ts +153 -12
  38. package/src/v4/mini/tests/codec.test.ts +499 -0
  39. package/src/v4/mini/tests/object.test.ts +9 -0
  40. package/src/v4/mini/tests/string.test.ts +16 -0
  41. package/v4/classic/external.cjs +1 -2
  42. package/v4/classic/external.d.cts +1 -1
  43. package/v4/classic/external.d.ts +1 -1
  44. package/v4/classic/external.js +1 -1
  45. package/v4/classic/parse.cjs +10 -1
  46. package/v4/classic/parse.d.cts +8 -0
  47. package/v4/classic/parse.d.ts +8 -0
  48. package/v4/classic/parse.js +9 -0
  49. package/v4/classic/schemas.cjs +59 -4
  50. package/v4/classic/schemas.d.cts +48 -2
  51. package/v4/classic/schemas.d.ts +48 -2
  52. package/v4/classic/schemas.js +51 -3
  53. package/v4/core/api.cjs +19 -24
  54. package/v4/core/api.d.cts +3 -4
  55. package/v4/core/api.d.ts +3 -4
  56. package/v4/core/api.js +19 -24
  57. package/v4/core/core.cjs +8 -1
  58. package/v4/core/core.d.cts +3 -0
  59. package/v4/core/core.d.ts +3 -0
  60. package/v4/core/core.js +6 -0
  61. package/v4/core/index.cjs +0 -1
  62. package/v4/core/index.d.cts +0 -1
  63. package/v4/core/index.d.ts +0 -1
  64. package/v4/core/index.js +0 -1
  65. package/v4/core/json-schema.d.cts +1 -0
  66. package/v4/core/json-schema.d.ts +1 -0
  67. package/v4/core/parse.cjs +45 -1
  68. package/v4/core/parse.d.cts +24 -0
  69. package/v4/core/parse.d.ts +24 -0
  70. package/v4/core/parse.js +36 -0
  71. package/v4/core/regexes.cjs +34 -2
  72. package/v4/core/regexes.d.cts +16 -0
  73. package/v4/core/regexes.d.ts +16 -0
  74. package/v4/core/regexes.js +32 -1
  75. package/v4/core/schemas.cjs +309 -77
  76. package/v4/core/schemas.d.cts +61 -3
  77. package/v4/core/schemas.d.ts +61 -3
  78. package/v4/core/schemas.js +308 -76
  79. package/v4/core/to-json-schema.cjs +42 -5
  80. package/v4/core/to-json-schema.d.cts +4 -3
  81. package/v4/core/to-json-schema.d.ts +4 -3
  82. package/v4/core/to-json-schema.js +42 -5
  83. package/v4/core/util.cjs +69 -0
  84. package/v4/core/util.d.cts +10 -0
  85. package/v4/core/util.d.ts +10 -0
  86. package/v4/core/util.js +62 -0
  87. package/v4/mini/external.cjs +1 -2
  88. package/v4/mini/external.d.cts +1 -1
  89. package/v4/mini/external.d.ts +1 -1
  90. package/v4/mini/external.js +1 -1
  91. package/v4/mini/parse.cjs +9 -1
  92. package/v4/mini/parse.d.cts +1 -1
  93. package/v4/mini/parse.d.ts +1 -1
  94. package/v4/mini/parse.js +1 -1
  95. package/v4/mini/schemas.cjs +58 -3
  96. package/v4/mini/schemas.d.cts +49 -1
  97. package/v4/mini/schemas.d.ts +49 -1
  98. package/v4/mini/schemas.js +49 -2
  99. package/src/v4/core/function.ts +0 -176
  100. package/v4/core/function.cjs +0 -102
  101. package/v4/core/function.d.cts +0 -52
  102. package/v4/core/function.d.ts +0 -52
  103. package/v4/core/function.js +0 -75
@@ -0,0 +1,538 @@
1
+ import { expect, test } from "vitest";
2
+ import * as z from "zod/v4";
3
+
4
+ // ============================================================================
5
+ // Number/BigInt Codecs
6
+ // ============================================================================
7
+
8
+ const stringToNumber = () =>
9
+ z.codec(z.string(), z.number(), {
10
+ decode: (str) => Number.parseFloat(str),
11
+ encode: (num) => num.toString(),
12
+ });
13
+
14
+ const stringToInt = () =>
15
+ z.codec(z.string(), z.int(), {
16
+ decode: (str) => Number.parseInt(str, 10),
17
+ encode: (num) => num.toString(),
18
+ });
19
+
20
+ const stringToBigInt = () =>
21
+ z.codec(z.string(), z.bigint(), {
22
+ decode: (str) => BigInt(str),
23
+ encode: (bigint) => bigint.toString(),
24
+ });
25
+
26
+ const numberToBigInt = () =>
27
+ z.codec(z.int(), z.bigint(), {
28
+ decode: (num) => BigInt(num),
29
+ encode: (bigint) => Number(bigint),
30
+ });
31
+
32
+ // ============================================================================
33
+ // Date/Duration Codecs
34
+ // ============================================================================
35
+
36
+ const isoDatetimeToDate = () =>
37
+ z.codec(z.iso.datetime(), z.date(), {
38
+ decode: (isoString) => new Date(isoString),
39
+ encode: (date) => date.toISOString(),
40
+ });
41
+
42
+ const epochSecondsToDate = () =>
43
+ z.codec(z.int().min(0), z.date(), {
44
+ decode: (seconds) => new Date(seconds * 1000),
45
+ encode: (date) => Math.floor(date.getTime() / 1000),
46
+ });
47
+
48
+ const epochMillisToDate = () =>
49
+ z.codec(z.int().min(0), z.date(), {
50
+ decode: (millis) => new Date(millis),
51
+ encode: (date) => date.getTime(),
52
+ });
53
+
54
+ // ============================================================================
55
+ // JSON Codec
56
+ // ============================================================================
57
+
58
+ const json = <T extends z.ZodTypeAny>(schema: T) =>
59
+ z.codec(z.string(), schema, {
60
+ decode: (jsonString) => JSON.parse(jsonString),
61
+ encode: (value) => JSON.stringify(value),
62
+ });
63
+
64
+ // ============================================================================
65
+ // Text/Bytes Codecs
66
+ // ============================================================================
67
+
68
+ const utf8ToBytes = () =>
69
+ z.codec(z.string(), z.instanceof(Uint8Array), {
70
+ decode: (str) => new TextEncoder().encode(str),
71
+ encode: (bytes) => new TextDecoder().decode(bytes),
72
+ });
73
+
74
+ const bytesToUtf8 = () =>
75
+ z.codec(z.instanceof(Uint8Array), z.string(), {
76
+ decode: (bytes) => new TextDecoder().decode(bytes),
77
+ encode: (str) => new TextEncoder().encode(str),
78
+ });
79
+
80
+ // ============================================================================
81
+ // Binary-to-text Codecs
82
+ // ============================================================================
83
+
84
+ // Using utility functions from z.core.util
85
+
86
+ const base64 = () =>
87
+ z.codec(z.base64(), z.instanceof(Uint8Array), {
88
+ decode: (base64String) => z.core.util.base64ToUint8Array(base64String),
89
+ encode: (bytes) => z.core.util.uint8ArrayToBase64(bytes),
90
+ });
91
+
92
+ const base64urlToBytes = () =>
93
+ z.codec(z.base64url(), z.instanceof(Uint8Array), {
94
+ decode: (base64urlString) => z.core.util.base64urlToUint8Array(base64urlString),
95
+ encode: (bytes) => z.core.util.uint8ArrayToBase64url(bytes),
96
+ });
97
+
98
+ const hexToBytes = () =>
99
+ z.codec(z.hex(), z.instanceof(Uint8Array), {
100
+ decode: (hexString) => z.core.util.hexToUint8Array(hexString),
101
+ encode: (bytes) => z.core.util.uint8ArrayToHex(bytes),
102
+ });
103
+
104
+ // ============================================================================
105
+ // URL Codecs
106
+ // ============================================================================
107
+
108
+ const stringToURL = () =>
109
+ z.codec(z.url(), z.instanceof(URL), {
110
+ decode: (urlString) => new URL(urlString),
111
+ encode: (url) => url.href,
112
+ });
113
+
114
+ const stringToHttpURL = () =>
115
+ z.codec(z.httpUrl(), z.instanceof(URL), {
116
+ decode: (urlString) => new URL(urlString),
117
+ encode: (url) => url.href,
118
+ });
119
+
120
+ const uriComponent = () =>
121
+ z.codec(z.string(), z.string(), {
122
+ decode: (encodedString) => decodeURIComponent(encodedString),
123
+ encode: (decodedString) => encodeURIComponent(decodedString),
124
+ });
125
+
126
+ // ============================================================================
127
+ // Boolean Codec
128
+ // ============================================================================
129
+
130
+ const stringToBoolean = (options?: { truthy?: string[]; falsy?: string[] }) => z.stringbool(options);
131
+
132
+ // ============================================================================
133
+ // Tests
134
+ // ============================================================================
135
+
136
+ test("stringToNumber codec", () => {
137
+ const codec = stringToNumber();
138
+
139
+ // Test decode
140
+ expect(z.decode(codec, "42.5")).toBe(42.5);
141
+ expect(z.decode(codec, "0")).toBe(0);
142
+ expect(z.decode(codec, "-123.456")).toBe(-123.456);
143
+
144
+ // Test encode
145
+ expect(z.encode(codec, 42.5)).toBe("42.5");
146
+ expect(z.encode(codec, 0)).toBe("0");
147
+ expect(z.encode(codec, -123.456)).toBe("-123.456");
148
+
149
+ // Test round trip
150
+ const original = "3.14159";
151
+ const roundTrip = z.encode(codec, z.decode(codec, original));
152
+ expect(roundTrip).toBe("3.14159");
153
+ });
154
+
155
+ test("stringToInt codec", () => {
156
+ const codec = stringToInt();
157
+
158
+ // Test decode
159
+ expect(z.decode(codec, "42")).toBe(42);
160
+ expect(z.decode(codec, "0")).toBe(0);
161
+ expect(z.decode(codec, "-123")).toBe(-123);
162
+
163
+ // Test encode
164
+ expect(z.encode(codec, 42)).toBe("42");
165
+ expect(z.encode(codec, 0)).toBe("0");
166
+ expect(z.encode(codec, -123)).toBe("-123");
167
+
168
+ // Test round trip
169
+ const original = "999";
170
+ const roundTrip = z.encode(codec, z.decode(codec, original));
171
+ expect(roundTrip).toBe("999");
172
+ });
173
+
174
+ test("stringToBigInt codec", () => {
175
+ const codec = stringToBigInt();
176
+
177
+ // Test decode
178
+ expect(z.decode(codec, "123456789012345678901234567890")).toBe(123456789012345678901234567890n);
179
+ expect(z.decode(codec, "0")).toBe(0n);
180
+ expect(z.decode(codec, "-999")).toBe(-999n);
181
+
182
+ // Test encode
183
+ expect(z.encode(codec, 123456789012345678901234567890n)).toBe("123456789012345678901234567890");
184
+ expect(z.encode(codec, 0n)).toBe("0");
185
+ expect(z.encode(codec, -999n)).toBe("-999");
186
+
187
+ // Test round trip
188
+ const original = "987654321098765432109876543210";
189
+ const roundTrip = z.encode(codec, z.decode(codec, original));
190
+ expect(roundTrip).toBe("987654321098765432109876543210");
191
+ });
192
+
193
+ test("numberToBigInt codec", () => {
194
+ const codec = numberToBigInt();
195
+
196
+ // Test decode
197
+ expect(z.decode(codec, 42)).toBe(42n);
198
+ expect(z.decode(codec, 0)).toBe(0n);
199
+ expect(z.decode(codec, -123)).toBe(-123n);
200
+
201
+ // Test encode
202
+ expect(z.encode(codec, 42n)).toBe(42);
203
+ expect(z.encode(codec, 0n)).toBe(0);
204
+ expect(z.encode(codec, -123n)).toBe(-123);
205
+
206
+ // Test round trip
207
+ const original = 999;
208
+ const roundTrip = z.encode(codec, z.decode(codec, original));
209
+ expect(roundTrip).toBe(999);
210
+ });
211
+
212
+ test("isoDatetimeToDate codec", () => {
213
+ const codec = isoDatetimeToDate();
214
+
215
+ // Test decode
216
+ const decoded = z.decode(codec, "2024-01-15T10:30:00.000Z");
217
+ expect(decoded).toBeInstanceOf(Date);
218
+ expect(decoded.getTime()).toBe(1705314600000);
219
+
220
+ // Test encode
221
+ const date = new Date("2024-01-15T10:30:00.000Z");
222
+ expect(z.encode(codec, date)).toBe("2024-01-15T10:30:00.000Z");
223
+
224
+ // Test round trip
225
+ const original = "2024-12-25T15:45:30.123Z";
226
+ const roundTrip = z.encode(codec, z.decode(codec, original));
227
+ expect(roundTrip).toBe("2024-12-25T15:45:30.123Z");
228
+ });
229
+
230
+ test("epochSecondsToDate codec", () => {
231
+ const codec = epochSecondsToDate();
232
+
233
+ // Test decode
234
+ const decoded = z.decode(codec, 1705314600);
235
+ expect(decoded).toBeInstanceOf(Date);
236
+ expect(decoded.getTime()).toBe(1705314600000);
237
+
238
+ // Test encode
239
+ const date = new Date(1705314600000);
240
+ expect(z.encode(codec, date)).toBe(1705314600);
241
+
242
+ // Test round trip
243
+ const original = 1640995200; // 2022-01-01 00:00:00 UTC
244
+ const roundTrip = z.encode(codec, z.decode(codec, original));
245
+ expect(roundTrip).toBe(1640995200);
246
+ });
247
+
248
+ test("epochMillisToDate codec", () => {
249
+ const codec = epochMillisToDate();
250
+
251
+ // Test decode
252
+ const decoded = z.decode(codec, 1705314600000);
253
+ expect(decoded).toBeInstanceOf(Date);
254
+ expect(decoded.getTime()).toBe(1705314600000);
255
+
256
+ // Test encode
257
+ const date = new Date(1705314600000);
258
+ expect(z.encode(codec, date)).toBe(1705314600000);
259
+
260
+ // Test round trip
261
+ const original = 1640995200123; // 2022-01-01 00:00:00.123 UTC
262
+ const roundTrip = z.encode(codec, z.decode(codec, original));
263
+ expect(roundTrip).toBe(1640995200123);
264
+ });
265
+
266
+ test("json codec", () => {
267
+ const codec = json(z.object({ name: z.string(), age: z.number() }));
268
+
269
+ // Test decode
270
+ const decoded = z.decode(codec, '{"name":"Alice","age":30}');
271
+ expect(decoded).toEqual({ name: "Alice", age: 30 });
272
+
273
+ // Test encode
274
+ const encoded = z.encode(codec, { name: "Bob", age: 25 });
275
+ expect(encoded).toBe('{"name":"Bob","age":25}');
276
+
277
+ // Test round trip
278
+ const original = '{"name":"Charlie","age":35}';
279
+ const parsed = z.decode(codec, original);
280
+ const roundTrip = z.encode(codec, parsed);
281
+ expect(JSON.parse(roundTrip)).toEqual(JSON.parse(original));
282
+ });
283
+
284
+ test("utf8ToBytes codec", () => {
285
+ const codec = utf8ToBytes();
286
+
287
+ // Test decode
288
+ const decoded = z.decode(codec, "Hello, 世界!");
289
+ expect(decoded).toBeInstanceOf(Uint8Array);
290
+ expect(Array.from(decoded)).toEqual([72, 101, 108, 108, 111, 44, 32, 228, 184, 150, 231, 149, 140, 33]);
291
+
292
+ // Test encode
293
+ const bytes = new Uint8Array([72, 101, 108, 108, 111]);
294
+ expect(z.encode(codec, bytes)).toBe("Hello");
295
+
296
+ // Test round trip
297
+ const original = "Hello, 世界! 🚀";
298
+ const roundTrip = z.encode(codec, z.decode(codec, original));
299
+ expect(roundTrip).toBe(original);
300
+ });
301
+
302
+ test("bytesToUtf8 codec", () => {
303
+ const codec = bytesToUtf8();
304
+
305
+ // Test decode
306
+ const bytes = new Uint8Array([72, 101, 108, 108, 111]);
307
+ const decoded = z.decode(codec, bytes);
308
+ expect(decoded).toBe("Hello");
309
+
310
+ // Test encode
311
+ const encoded = z.encode(codec, "Hello, 世界!");
312
+ expect(encoded).toBeInstanceOf(Uint8Array);
313
+ expect(Array.from(encoded)).toEqual([72, 101, 108, 108, 111, 44, 32, 228, 184, 150, 231, 149, 140, 33]);
314
+
315
+ // Test round trip
316
+ const original = new Uint8Array([72, 101, 108, 108, 111, 44, 32, 228, 184, 150, 231, 149, 140, 33]);
317
+ const roundTrip = z.encode(codec, z.decode(codec, original));
318
+ expect(roundTrip).toEqual(original);
319
+ });
320
+
321
+ test("base64 codec", () => {
322
+ const codec = base64();
323
+
324
+ // Test decode
325
+ const decoded = z.decode(codec, "SGVsbG8=");
326
+ expect(decoded).toBeInstanceOf(Uint8Array);
327
+ expect(Array.from(decoded)).toEqual([72, 101, 108, 108, 111]);
328
+
329
+ // Test encode
330
+ const bytes = new Uint8Array([72, 101, 108, 108, 111]);
331
+ expect(z.encode(codec, bytes)).toBe("SGVsbG8=");
332
+
333
+ // Test round trip
334
+ const original = "SGVsbG8gV29ybGQh";
335
+ const roundTrip = z.encode(codec, z.decode(codec, original));
336
+ expect(roundTrip).toBe(original);
337
+ });
338
+
339
+ test("base64urlToBytes codec", () => {
340
+ const codec = base64urlToBytes();
341
+
342
+ // Test decode
343
+ const decoded = z.decode(codec, "SGVsbG8");
344
+ expect(decoded).toBeInstanceOf(Uint8Array);
345
+ expect(Array.from(decoded)).toEqual([72, 101, 108, 108, 111]);
346
+
347
+ // Test encode
348
+ const bytes = new Uint8Array([72, 101, 108, 108, 111]);
349
+ expect(z.encode(codec, bytes)).toBe("SGVsbG8");
350
+
351
+ // Test round trip with padding case
352
+ const original = "SGVsbG9Xb3JsZA";
353
+ const roundTrip = z.encode(codec, z.decode(codec, original));
354
+ expect(roundTrip).toBe(original);
355
+ });
356
+
357
+ test("hexToBytes codec", () => {
358
+ const codec = hexToBytes();
359
+
360
+ // Test decode
361
+ const decoded = z.decode(codec, "48656c6c6f");
362
+ expect(decoded).toBeInstanceOf(Uint8Array);
363
+ expect(Array.from(decoded)).toEqual([72, 101, 108, 108, 111]);
364
+
365
+ // Note: z.hex() doesn't accept 0x prefix, but our utility function can handle it
366
+ // const decodedWithPrefix = z.decode(codec, "0x48656c6c6f");
367
+ // expect(Array.from(decodedWithPrefix)).toEqual([72, 101, 108, 108, 111]);
368
+
369
+ // Test encode
370
+ const bytes = new Uint8Array([72, 101, 108, 108, 111]);
371
+ expect(z.encode(codec, bytes)).toBe("48656c6c6f");
372
+
373
+ // Test round trip
374
+ const original = "deadbeef";
375
+ const roundTrip = z.encode(codec, z.decode(codec, original));
376
+ expect(roundTrip).toBe("deadbeef");
377
+ });
378
+
379
+ test("stringToURL codec", () => {
380
+ const codec = stringToURL();
381
+
382
+ // Test decode
383
+ const decoded = z.decode(codec, "https://example.com/path?query=value");
384
+ expect(decoded).toBeInstanceOf(URL);
385
+ expect(decoded.hostname).toBe("example.com");
386
+ expect(decoded.pathname).toBe("/path");
387
+ expect(decoded.search).toBe("?query=value");
388
+
389
+ // Test encode
390
+ const url = new URL("https://example.com/path?query=value");
391
+ expect(z.encode(codec, url)).toBe("https://example.com/path?query=value");
392
+
393
+ // Test round trip
394
+ const original = "https://test.com/api/v1?foo=bar&baz=qux";
395
+ const roundTrip = z.encode(codec, z.decode(codec, original));
396
+ expect(roundTrip).toBe(original);
397
+ });
398
+
399
+ test("stringToHttpURL codec", () => {
400
+ const codec = stringToHttpURL();
401
+
402
+ // Test decode HTTPS
403
+ const decodedHttps = z.decode(codec, "https://example.com/path");
404
+ expect(decodedHttps).toBeInstanceOf(URL);
405
+ expect(decodedHttps.protocol).toBe("https:");
406
+
407
+ // Test decode HTTP
408
+ const decodedHttp = z.decode(codec, "http://example.com/path");
409
+ expect(decodedHttp).toBeInstanceOf(URL);
410
+ expect(decodedHttp.protocol).toBe("http:");
411
+
412
+ // Test encode
413
+ const url = new URL("https://example.com/path");
414
+ expect(z.encode(codec, url)).toBe("https://example.com/path");
415
+
416
+ // Test round trip
417
+ const original = "http://api.example.com/v1/users";
418
+ const roundTrip = z.encode(codec, z.decode(codec, original));
419
+ expect(roundTrip).toBe(original);
420
+ });
421
+
422
+ test("uriComponent codec", () => {
423
+ const codec = uriComponent();
424
+
425
+ // Test decode
426
+ const decoded = z.decode(codec, "Hello%20World%21");
427
+ expect(decoded).toBe("Hello World!");
428
+
429
+ // Test encode
430
+ const encoded = z.encode(codec, "Hello World!");
431
+ expect(encoded).toBe("Hello%20World!");
432
+
433
+ // Test round trip
434
+ const original = "Hello%20World%21%20%26%20More";
435
+ const roundTrip = z.encode(codec, z.decode(codec, original));
436
+ expect(roundTrip).toBe("Hello%20World!%20%26%20More");
437
+
438
+ // Test complex characters
439
+ const complex = "café & résumé";
440
+ const encodedComplex = z.encode(codec, complex);
441
+ const decodedComplex = z.decode(codec, encodedComplex);
442
+ expect(decodedComplex).toBe(complex);
443
+ });
444
+
445
+ test("stringToBoolean codec", () => {
446
+ const codec = stringToBoolean();
447
+
448
+ // Test decode - default truthy values
449
+ expect(z.decode(codec, "true")).toBe(true);
450
+ expect(z.decode(codec, "yes")).toBe(true);
451
+ expect(z.decode(codec, "1")).toBe(true);
452
+
453
+ // Test decode - default falsy values
454
+ expect(z.decode(codec, "false")).toBe(false);
455
+ expect(z.decode(codec, "no")).toBe(false);
456
+ expect(z.decode(codec, "0")).toBe(false);
457
+
458
+ // Test encode - default behavior
459
+ expect(z.encode(codec, true)).toBe("true");
460
+ expect(z.encode(codec, false)).toBe("false");
461
+
462
+ // Test custom options
463
+ const customCodec = stringToBoolean({ truthy: ["yes", "y"], falsy: ["no", "n"] });
464
+ expect(z.decode(customCodec, "yes")).toBe(true);
465
+ expect(z.decode(customCodec, "y")).toBe(true);
466
+ expect(z.decode(customCodec, "no")).toBe(false);
467
+ expect(z.decode(customCodec, "n")).toBe(false);
468
+ expect(z.encode(customCodec, true)).toBe("yes");
469
+ expect(z.encode(customCodec, false)).toBe("no");
470
+ });
471
+
472
+ // Test error cases - these test input validation, not transform errors
473
+ test("codec input validation", () => {
474
+ // Test invalid base64 format
475
+ const base64Codec = base64();
476
+ const invalidBase64Result = z.safeDecode(base64Codec, "invalid!@#");
477
+ expect(invalidBase64Result.success).toBe(false);
478
+
479
+ // Test invalid hex format
480
+ const hexCodec = hexToBytes();
481
+ const invalidHexResult = z.safeDecode(hexCodec, "gg");
482
+ expect(invalidHexResult.success).toBe(false);
483
+
484
+ // Test invalid URL format
485
+ const urlCodec = stringToURL();
486
+ const invalidUrlResult = z.safeDecode(urlCodec, "not a url");
487
+ expect(invalidUrlResult.success).toBe(false);
488
+
489
+ // Test invalid HTTP URL format
490
+ const httpUrlCodec = stringToHttpURL();
491
+ const invalidHttpResult = z.safeDecode(httpUrlCodec, "ftp://example.com");
492
+ expect(invalidHttpResult.success).toBe(false);
493
+ });
494
+
495
+ // Test transform errors - these test errors added by transform functions
496
+ test("codec transform error handling", () => {
497
+ // JSON codec that can fail during transform
498
+ const jsonCodec = z.codec(z.string(), z.json(), {
499
+ decode: (jsonString, ctx) => {
500
+ try {
501
+ return JSON.parse(jsonString);
502
+ } catch (err: any) {
503
+ ctx.issues.push({
504
+ code: "invalid_format",
505
+ format: "json",
506
+ input: jsonString,
507
+ message: err.message,
508
+ });
509
+ return z.NEVER;
510
+ }
511
+ },
512
+ encode: (value) => JSON.stringify(value),
513
+ });
514
+
515
+ // Test successful JSON parsing
516
+ const validResult = z.safeDecode(jsonCodec, '{"valid": "json"}');
517
+ expect(validResult.success).toBe(true);
518
+ if (validResult.success) {
519
+ expect(validResult.data).toEqual({ valid: "json" });
520
+ }
521
+
522
+ // Test invalid JSON that should create a single "invalid_format" issue
523
+ // Verifies that the transform error aborts before reaching the output schema
524
+ const invalidResult = z.safeDecode(jsonCodec, '{"invalid":,}');
525
+ expect(invalidResult.success).toBe(false);
526
+ if (!invalidResult.success) {
527
+ expect(invalidResult.error.issues).toMatchInlineSnapshot(`
528
+ [
529
+ {
530
+ "code": "invalid_format",
531
+ "format": "json",
532
+ "message": "Unexpected token ',', "{"invalid":,}" is not valid JSON",
533
+ "path": [],
534
+ },
535
+ ]
536
+ `);
537
+ }
538
+ });