anyvali 0.3.1

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 (204) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +370 -0
  3. package/VERSION +1 -0
  4. package/dist/errors.d.ts +6 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +12 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/format/validators.d.ts +2 -0
  9. package/dist/format/validators.d.ts.map +1 -0
  10. package/dist/format/validators.js +57 -0
  11. package/dist/format/validators.js.map +1 -0
  12. package/dist/forms/index.d.ts +57 -0
  13. package/dist/forms/index.d.ts.map +1 -0
  14. package/dist/forms/index.js +586 -0
  15. package/dist/forms/index.js.map +1 -0
  16. package/dist/index.d.ts +93 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +156 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/infer.d.ts +8 -0
  21. package/dist/infer.d.ts.map +1 -0
  22. package/dist/infer.js +2 -0
  23. package/dist/infer.js.map +1 -0
  24. package/dist/interchange/document.d.ts +5 -0
  25. package/dist/interchange/document.d.ts.map +1 -0
  26. package/dist/interchange/document.js +12 -0
  27. package/dist/interchange/document.js.map +1 -0
  28. package/dist/interchange/exporter.d.ts +7 -0
  29. package/dist/interchange/exporter.d.ts.map +1 -0
  30. package/dist/interchange/exporter.js +7 -0
  31. package/dist/interchange/exporter.js.map +1 -0
  32. package/dist/interchange/importer.d.ts +4 -0
  33. package/dist/interchange/importer.d.ts.map +1 -0
  34. package/dist/interchange/importer.js +229 -0
  35. package/dist/interchange/importer.js.map +1 -0
  36. package/dist/issue-codes.d.ts +19 -0
  37. package/dist/issue-codes.d.ts.map +1 -0
  38. package/dist/issue-codes.js +18 -0
  39. package/dist/issue-codes.js.map +1 -0
  40. package/dist/parse/coerce.d.ts +16 -0
  41. package/dist/parse/coerce.d.ts.map +1 -0
  42. package/dist/parse/coerce.js +115 -0
  43. package/dist/parse/coerce.js.map +1 -0
  44. package/dist/parse/defaults.d.ts +7 -0
  45. package/dist/parse/defaults.d.ts.map +1 -0
  46. package/dist/parse/defaults.js +12 -0
  47. package/dist/parse/defaults.js.map +1 -0
  48. package/dist/parse/parser.d.ts +11 -0
  49. package/dist/parse/parser.d.ts.map +1 -0
  50. package/dist/parse/parser.js +13 -0
  51. package/dist/parse/parser.js.map +1 -0
  52. package/dist/schemas/any.d.ts +7 -0
  53. package/dist/schemas/any.d.ts.map +1 -0
  54. package/dist/schemas/any.js +12 -0
  55. package/dist/schemas/any.js.map +1 -0
  56. package/dist/schemas/array.d.ts +13 -0
  57. package/dist/schemas/array.d.ts.map +1 -0
  58. package/dist/schemas/array.js +73 -0
  59. package/dist/schemas/array.js.map +1 -0
  60. package/dist/schemas/base.d.ts +37 -0
  61. package/dist/schemas/base.d.ts.map +1 -0
  62. package/dist/schemas/base.js +285 -0
  63. package/dist/schemas/base.js.map +1 -0
  64. package/dist/schemas/bool.d.ts +8 -0
  65. package/dist/schemas/bool.d.ts.map +1 -0
  66. package/dist/schemas/bool.js +27 -0
  67. package/dist/schemas/bool.js.map +1 -0
  68. package/dist/schemas/enum.d.ts +9 -0
  69. package/dist/schemas/enum.d.ts.map +1 -0
  70. package/dist/schemas/enum.js +31 -0
  71. package/dist/schemas/enum.js.map +1 -0
  72. package/dist/schemas/index.d.ts +21 -0
  73. package/dist/schemas/index.d.ts.map +1 -0
  74. package/dist/schemas/index.js +21 -0
  75. package/dist/schemas/index.js.map +1 -0
  76. package/dist/schemas/int.d.ts +32 -0
  77. package/dist/schemas/int.d.ts.map +1 -0
  78. package/dist/schemas/int.js +108 -0
  79. package/dist/schemas/int.js.map +1 -0
  80. package/dist/schemas/intersection.d.ts +16 -0
  81. package/dist/schemas/intersection.d.ts.map +1 -0
  82. package/dist/schemas/intersection.js +58 -0
  83. package/dist/schemas/intersection.js.map +1 -0
  84. package/dist/schemas/literal.d.ts +11 -0
  85. package/dist/schemas/literal.d.ts.map +1 -0
  86. package/dist/schemas/literal.js +28 -0
  87. package/dist/schemas/literal.js.map +1 -0
  88. package/dist/schemas/never.d.ts +7 -0
  89. package/dist/schemas/never.d.ts.map +1 -0
  90. package/dist/schemas/never.js +19 -0
  91. package/dist/schemas/never.js.map +1 -0
  92. package/dist/schemas/null.d.ts +7 -0
  93. package/dist/schemas/null.d.ts.map +1 -0
  94. package/dist/schemas/null.js +24 -0
  95. package/dist/schemas/null.js.map +1 -0
  96. package/dist/schemas/nullable.d.ts +10 -0
  97. package/dist/schemas/nullable.d.ts.map +1 -0
  98. package/dist/schemas/nullable.js +29 -0
  99. package/dist/schemas/nullable.js.map +1 -0
  100. package/dist/schemas/number.d.ts +27 -0
  101. package/dist/schemas/number.d.ts.map +1 -0
  102. package/dist/schemas/number.js +134 -0
  103. package/dist/schemas/number.js.map +1 -0
  104. package/dist/schemas/object.d.ts +28 -0
  105. package/dist/schemas/object.d.ts.map +1 -0
  106. package/dist/schemas/object.js +153 -0
  107. package/dist/schemas/object.js.map +1 -0
  108. package/dist/schemas/optional.d.ts +11 -0
  109. package/dist/schemas/optional.d.ts.map +1 -0
  110. package/dist/schemas/optional.js +39 -0
  111. package/dist/schemas/optional.js.map +1 -0
  112. package/dist/schemas/record.d.ts +9 -0
  113. package/dist/schemas/record.d.ts.map +1 -0
  114. package/dist/schemas/record.js +45 -0
  115. package/dist/schemas/record.js.map +1 -0
  116. package/dist/schemas/ref.d.ts +10 -0
  117. package/dist/schemas/ref.d.ts.map +1 -0
  118. package/dist/schemas/ref.js +30 -0
  119. package/dist/schemas/ref.js.map +1 -0
  120. package/dist/schemas/string.d.ts +29 -0
  121. package/dist/schemas/string.d.ts.map +1 -0
  122. package/dist/schemas/string.js +181 -0
  123. package/dist/schemas/string.js.map +1 -0
  124. package/dist/schemas/tuple.d.ts +14 -0
  125. package/dist/schemas/tuple.d.ts.map +1 -0
  126. package/dist/schemas/tuple.js +59 -0
  127. package/dist/schemas/tuple.js.map +1 -0
  128. package/dist/schemas/union.d.ts +9 -0
  129. package/dist/schemas/union.d.ts.map +1 -0
  130. package/dist/schemas/union.js +45 -0
  131. package/dist/schemas/union.js.map +1 -0
  132. package/dist/schemas/unknown.d.ts +7 -0
  133. package/dist/schemas/unknown.d.ts.map +1 -0
  134. package/dist/schemas/unknown.js +12 -0
  135. package/dist/schemas/unknown.js.map +1 -0
  136. package/dist/types.d.ts +132 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +3 -0
  139. package/dist/types.js.map +1 -0
  140. package/dist/util.d.ts +6 -0
  141. package/dist/util.d.ts.map +1 -0
  142. package/dist/util.js +12 -0
  143. package/dist/util.js.map +1 -0
  144. package/package.json +41 -0
  145. package/sdk/js/CHANGELOG.md +13 -0
  146. package/src/errors.ts +17 -0
  147. package/src/format/validators.ts +71 -0
  148. package/src/forms/index.ts +789 -0
  149. package/src/index.ts +285 -0
  150. package/src/infer.ts +12 -0
  151. package/src/interchange/document.ts +18 -0
  152. package/src/interchange/exporter.ts +12 -0
  153. package/src/interchange/importer.ts +285 -0
  154. package/src/issue-codes.ts +19 -0
  155. package/src/parse/coerce.ts +133 -0
  156. package/src/parse/defaults.ts +15 -0
  157. package/src/parse/parser.ts +19 -0
  158. package/src/schemas/any.ts +14 -0
  159. package/src/schemas/array.ts +83 -0
  160. package/src/schemas/base.ts +322 -0
  161. package/src/schemas/bool.ts +30 -0
  162. package/src/schemas/enum.ts +37 -0
  163. package/src/schemas/index.ts +30 -0
  164. package/src/schemas/int.ts +129 -0
  165. package/src/schemas/intersection.ts +81 -0
  166. package/src/schemas/literal.ts +34 -0
  167. package/src/schemas/never.ts +21 -0
  168. package/src/schemas/null.ts +26 -0
  169. package/src/schemas/nullable.ts +36 -0
  170. package/src/schemas/number.ts +151 -0
  171. package/src/schemas/object.ts +203 -0
  172. package/src/schemas/optional.ts +49 -0
  173. package/src/schemas/record.ts +55 -0
  174. package/src/schemas/ref.ts +35 -0
  175. package/src/schemas/string.ts +192 -0
  176. package/src/schemas/tuple.ts +74 -0
  177. package/src/schemas/union.ts +53 -0
  178. package/src/schemas/unknown.ts +14 -0
  179. package/src/types.ts +239 -0
  180. package/src/util.ts +9 -0
  181. package/tests/conformance/runner.test.ts +28 -0
  182. package/tests/conformance/runner.ts +137 -0
  183. package/tests/forms.test.ts +146 -0
  184. package/tests/unit/coerce.test.ts +136 -0
  185. package/tests/unit/collections.test.ts +99 -0
  186. package/tests/unit/composition.test.ts +80 -0
  187. package/tests/unit/date-format.test.ts +18 -0
  188. package/tests/unit/default-mutation.test.ts +32 -0
  189. package/tests/unit/defaults.test.ts +49 -0
  190. package/tests/unit/errors.test.ts +53 -0
  191. package/tests/unit/export.test.ts +270 -0
  192. package/tests/unit/inference.test.ts +306 -0
  193. package/tests/unit/interchange.test.ts +191 -0
  194. package/tests/unit/number.test.ts +195 -0
  195. package/tests/unit/object.test.ts +208 -0
  196. package/tests/unit/parser.test.ts +151 -0
  197. package/tests/unit/primitives.test.ts +111 -0
  198. package/tests/unit/security-recursion.test.ts +105 -0
  199. package/tests/unit/security.test.ts +945 -0
  200. package/tests/unit/shared-ref-falsepos.test.ts +33 -0
  201. package/tests/unit/string-pattern-redos.test.ts +46 -0
  202. package/tests/unit/string.test.ts +147 -0
  203. package/tsconfig.json +21 -0
  204. package/vitest.config.ts +7 -0
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ string,
4
+ int,
5
+ object,
6
+ array,
7
+ optional,
8
+ nullable,
9
+ bool,
10
+ exportSchema,
11
+ importSchema,
12
+ } from "../../src/index.js";
13
+
14
+ describe("Export", () => {
15
+ it("exports a simple string schema", () => {
16
+ const doc = exportSchema(string().minLength(1).maxLength(100));
17
+ expect(doc.anyvaliVersion).toBe("1.0");
18
+ expect(doc.schemaVersion).toBe("1.1");
19
+ expect(doc.root.kind).toBe("string");
20
+ expect((doc.root as any).minLength).toBe(1);
21
+ expect((doc.root as any).maxLength).toBe(100);
22
+ });
23
+
24
+ it("exports an object schema", () => {
25
+ const s = object({
26
+ name: string(),
27
+ age: optional(int()),
28
+ });
29
+ const doc = exportSchema(s);
30
+ expect(doc.root.kind).toBe("object");
31
+ const root = doc.root as any;
32
+ expect(root.properties.name.kind).toBe("string");
33
+ expect(root.required).toContain("name");
34
+ expect(root.required).not.toContain("age");
35
+ });
36
+
37
+ it("exports defaults", () => {
38
+ const s = string().default("hello");
39
+ const doc = exportSchema(s);
40
+ expect(doc.root.default).toBe("hello");
41
+ });
42
+
43
+ it("exports coercion config", () => {
44
+ const s = int().coerce({ from: "string" });
45
+ const doc = exportSchema(s);
46
+ expect(doc.root.coerce).toEqual({ from: "string" });
47
+ });
48
+ });
49
+
50
+ describe("Import", () => {
51
+ it("round-trips a string schema", () => {
52
+ const original = string().minLength(1).maxLength(100);
53
+ const doc = exportSchema(original);
54
+ const imported = importSchema(doc);
55
+ expect(imported.parse("hello")).toBe("hello");
56
+ expect(imported.safeParse("").success).toBe(false);
57
+ });
58
+
59
+ it("round-trips an object schema", () => {
60
+ const original = object({
61
+ name: string(),
62
+ active: bool().default(true),
63
+ });
64
+ const doc = exportSchema(original);
65
+ const imported = importSchema(doc);
66
+ const result = imported.parse({ name: "Alice" });
67
+ expect(result).toEqual({ name: "Alice", active: true });
68
+ });
69
+
70
+ it("round-trips an array schema", () => {
71
+ const original = array(int()).minItems(1);
72
+ const doc = exportSchema(original);
73
+ const imported = importSchema(doc);
74
+ expect(imported.parse([1, 2, 3])).toEqual([1, 2, 3]);
75
+ expect(imported.safeParse([]).success).toBe(false);
76
+ });
77
+
78
+ it("round-trips nullable", () => {
79
+ const original = nullable(string());
80
+ const doc = exportSchema(original);
81
+ const imported = importSchema(doc);
82
+ expect(imported.parse(null)).toBe(null);
83
+ expect(imported.parse("hello")).toBe("hello");
84
+ });
85
+
86
+ it("round-trips coercion", () => {
87
+ const original = int().coerce({ from: "string" });
88
+ const doc = exportSchema(original);
89
+ const imported = importSchema(doc);
90
+ expect(imported.parse("42")).toBe(42);
91
+ });
92
+
93
+ it("throws on missing kind field", () => {
94
+ const doc = {
95
+ anyvaliVersion: "1.0",
96
+ schemaVersion: "1",
97
+ root: {},
98
+ definitions: {},
99
+ extensions: {},
100
+ };
101
+ expect(() => importSchema(doc as any)).toThrow();
102
+ });
103
+
104
+ it("throws on null/empty root", () => {
105
+ expect(() =>
106
+ importSchema({ anyvaliVersion: "1.0", schemaVersion: "1", root: null } as any)
107
+ ).toThrow();
108
+ expect(() =>
109
+ importSchema({ anyvaliVersion: "1.0", schemaVersion: "1" } as any)
110
+ ).toThrow();
111
+ });
112
+
113
+ it("imports invalid regex patterns without throwing", () => {
114
+ const doc = {
115
+ anyvaliVersion: "1.0",
116
+ schemaVersion: "1.1",
117
+ root: { kind: "string", pattern: "(" },
118
+ definitions: {},
119
+ extensions: {},
120
+ };
121
+
122
+ const imported = importSchema(doc as any);
123
+
124
+ expect(() => imported.safeParse("abc")).not.toThrow();
125
+ expect(imported.safeParse("abc").success).toBe(false);
126
+ });
127
+
128
+ it("imports array schemas with canonical items keys", () => {
129
+ const doc = {
130
+ anyvaliVersion: "1.0",
131
+ schemaVersion: "1.1",
132
+ root: {
133
+ kind: "array",
134
+ items: { kind: "int" },
135
+ },
136
+ definitions: {},
137
+ extensions: {},
138
+ };
139
+
140
+ const imported = importSchema(doc as any);
141
+ expect(imported.parse([1, 2, 3])).toEqual([1, 2, 3]);
142
+ expect(imported.safeParse(["a"]).success).toBe(false);
143
+ });
144
+
145
+ it("imports union schemas with canonical variants keys", () => {
146
+ const doc = {
147
+ anyvaliVersion: "1.0",
148
+ schemaVersion: "1.1",
149
+ root: {
150
+ kind: "union",
151
+ variants: [{ kind: "string" }, { kind: "int" }],
152
+ },
153
+ definitions: {},
154
+ extensions: {},
155
+ };
156
+
157
+ const imported = importSchema(doc as any);
158
+ expect(imported.parse("hello")).toBe("hello");
159
+ expect(imported.parse(42)).toBe(42);
160
+ expect(imported.safeParse(true).success).toBe(false);
161
+ });
162
+
163
+ it("imports __proto__ property names without prototype pollution", () => {
164
+ const doc = JSON.parse(`{
165
+ "anyvaliVersion": "1.0",
166
+ "schemaVersion": "1.1",
167
+ "root": {
168
+ "kind": "object",
169
+ "properties": {
170
+ "__proto__": { "kind": "string" }
171
+ },
172
+ "required": ["__proto__"],
173
+ "unknownKeys": "reject"
174
+ },
175
+ "definitions": {},
176
+ "extensions": {}
177
+ }`);
178
+ const input = JSON.parse('{"__proto__":"safe"}') as Record<string, unknown>;
179
+
180
+ const imported = importSchema(doc as any);
181
+
182
+ expect(() => imported.parse(input)).not.toThrow();
183
+ const result = imported.parse(input) as Record<string, unknown>;
184
+ expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
185
+ expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(true);
186
+ expect(Object.getOwnPropertyDescriptor(result, "__proto__")?.value).toBe(
187
+ "safe"
188
+ );
189
+ expect(({} as Record<string, unknown>).safe).toBeUndefined();
190
+ });
191
+ });
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { number, float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64 } from "../../src/index.js";
3
+
4
+ describe("NumberSchema", () => {
5
+ it("accepts valid numbers", () => {
6
+ expect(number().parse(42)).toBe(42);
7
+ expect(number().parse(3.14)).toBe(3.14);
8
+ expect(number().parse(-0.5)).toBe(-0.5);
9
+ });
10
+
11
+ it("rejects non-numbers", () => {
12
+ const result = number().safeParse("42");
13
+ expect(result.success).toBe(false);
14
+ if (!result.success) expect(result.issues[0].code).toBe("invalid_type");
15
+ });
16
+
17
+ it("rejects NaN and Infinity", () => {
18
+ expect(number().safeParse(NaN).success).toBe(false);
19
+ expect(number().safeParse(Infinity).success).toBe(false);
20
+ expect(number().safeParse(-Infinity).success).toBe(false);
21
+ });
22
+
23
+ it("validates min", () => {
24
+ const s = number().min(5);
25
+ expect(s.parse(5)).toBe(5);
26
+ expect(s.safeParse(4).success).toBe(false);
27
+ });
28
+
29
+ it("validates max", () => {
30
+ const s = number().max(10);
31
+ expect(s.parse(10)).toBe(10);
32
+ expect(s.safeParse(11).success).toBe(false);
33
+ });
34
+
35
+ it("validates exclusiveMin", () => {
36
+ const s = number().exclusiveMin(5);
37
+ expect(s.parse(6)).toBe(6);
38
+ expect(s.safeParse(5).success).toBe(false);
39
+ });
40
+
41
+ it("validates exclusiveMax", () => {
42
+ const s = number().exclusiveMax(10);
43
+ expect(s.parse(9)).toBe(9);
44
+ expect(s.safeParse(10).success).toBe(false);
45
+ });
46
+
47
+ it("validates multipleOf", () => {
48
+ const s = number().multipleOf(3);
49
+ expect(s.parse(9)).toBe(9);
50
+ expect(s.safeParse(10).success).toBe(false);
51
+ });
52
+
53
+ it("coerces string to number", () => {
54
+ const s = number().coerce({ from: "string" });
55
+ expect(s.parse("3.14")).toBe(3.14);
56
+ });
57
+
58
+ it("coercion failure", () => {
59
+ const s = number().coerce({ from: "string" });
60
+ const result = s.safeParse("abc");
61
+ expect(result.success).toBe(false);
62
+ if (!result.success) expect(result.issues[0].code).toBe("coercion_failed");
63
+ });
64
+ });
65
+
66
+ describe("Float32Schema", () => {
67
+ it("accepts valid numbers", () => {
68
+ expect(float32().parse(1.5)).toBe(1.5);
69
+ });
70
+ });
71
+
72
+ describe("Float64Schema", () => {
73
+ it("accepts valid numbers", () => {
74
+ expect(float64().parse(1.5)).toBe(1.5);
75
+ });
76
+ });
77
+
78
+ describe("IntSchema", () => {
79
+ it("accepts integers", () => {
80
+ expect(int().parse(42)).toBe(42);
81
+ expect(int().parse(-10)).toBe(-10);
82
+ expect(int().parse(0)).toBe(0);
83
+ });
84
+
85
+ it("rejects floats", () => {
86
+ const result = int().safeParse(3.14);
87
+ expect(result.success).toBe(false);
88
+ if (!result.success) expect(result.issues[0].code).toBe("invalid_type");
89
+ });
90
+
91
+ it("rejects non-numbers", () => {
92
+ expect(int().safeParse("42").success).toBe(false);
93
+ });
94
+
95
+ it("coerces string to int", () => {
96
+ const s = int().coerce({ from: "string" });
97
+ expect(s.parse("42")).toBe(42);
98
+ });
99
+
100
+ it("rejects float strings when coercing to int", () => {
101
+ const s = int().coerce({ from: "string" });
102
+ const result = s.safeParse("3.14");
103
+ expect(result.success).toBe(false);
104
+ });
105
+ });
106
+
107
+ describe("Int8Schema", () => {
108
+ it("accepts values in range [-128, 127]", () => {
109
+ expect(int8().parse(0)).toBe(0);
110
+ expect(int8().parse(-128)).toBe(-128);
111
+ expect(int8().parse(127)).toBe(127);
112
+ });
113
+
114
+ it("rejects out-of-range values", () => {
115
+ expect(int8().safeParse(128).success).toBe(false);
116
+ expect(int8().safeParse(-129).success).toBe(false);
117
+ });
118
+ });
119
+
120
+ describe("Int16Schema", () => {
121
+ it("accepts values in range", () => {
122
+ expect(int16().parse(0)).toBe(0);
123
+ expect(int16().parse(-32768)).toBe(-32768);
124
+ expect(int16().parse(32767)).toBe(32767);
125
+ });
126
+
127
+ it("rejects out-of-range values", () => {
128
+ expect(int16().safeParse(32768).success).toBe(false);
129
+ });
130
+ });
131
+
132
+ describe("Int32Schema", () => {
133
+ it("accepts values in range", () => {
134
+ expect(int32().parse(0)).toBe(0);
135
+ expect(int32().parse(2147483647)).toBe(2147483647);
136
+ expect(int32().parse(-2147483648)).toBe(-2147483648);
137
+ });
138
+
139
+ it("rejects out-of-range values", () => {
140
+ expect(int32().safeParse(2147483648).success).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe("Int64Schema", () => {
145
+ it("accepts safe integers", () => {
146
+ expect(int64().parse(0)).toBe(0);
147
+ expect(int64().parse(Number.MAX_SAFE_INTEGER)).toBe(Number.MAX_SAFE_INTEGER);
148
+ expect(int64().parse(Number.MIN_SAFE_INTEGER)).toBe(Number.MIN_SAFE_INTEGER);
149
+ });
150
+ });
151
+
152
+ describe("Uint8Schema", () => {
153
+ it("accepts values in range [0, 255]", () => {
154
+ expect(uint8().parse(0)).toBe(0);
155
+ expect(uint8().parse(255)).toBe(255);
156
+ });
157
+
158
+ it("rejects negative and out-of-range", () => {
159
+ expect(uint8().safeParse(-1).success).toBe(false);
160
+ expect(uint8().safeParse(256).success).toBe(false);
161
+ });
162
+ });
163
+
164
+ describe("Uint16Schema", () => {
165
+ it("accepts values in range [0, 65535]", () => {
166
+ expect(uint16().parse(0)).toBe(0);
167
+ expect(uint16().parse(65535)).toBe(65535);
168
+ });
169
+
170
+ it("rejects out-of-range", () => {
171
+ expect(uint16().safeParse(65536).success).toBe(false);
172
+ });
173
+ });
174
+
175
+ describe("Uint32Schema", () => {
176
+ it("accepts values in range [0, 4294967295]", () => {
177
+ expect(uint32().parse(0)).toBe(0);
178
+ expect(uint32().parse(4294967295)).toBe(4294967295);
179
+ });
180
+
181
+ it("rejects out-of-range", () => {
182
+ expect(uint32().safeParse(4294967296).success).toBe(false);
183
+ });
184
+ });
185
+
186
+ describe("Uint64Schema", () => {
187
+ it("accepts values in range [0, MAX_SAFE_INTEGER]", () => {
188
+ expect(uint64().parse(0)).toBe(0);
189
+ expect(uint64().parse(Number.MAX_SAFE_INTEGER)).toBe(Number.MAX_SAFE_INTEGER);
190
+ });
191
+
192
+ it("rejects negative values", () => {
193
+ expect(uint64().safeParse(-1).success).toBe(false);
194
+ });
195
+ });
@@ -0,0 +1,208 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { object, string, int, optional, bool, RefSchema } from "../../src/index.js";
3
+
4
+ describe("ObjectSchema", () => {
5
+ it("accepts valid objects", () => {
6
+ const s = object({
7
+ name: string(),
8
+ age: int(),
9
+ });
10
+ const result = s.parse({ name: "Alice", age: 30 });
11
+ expect(result).toEqual({ name: "Alice", age: 30 });
12
+ });
13
+
14
+ it("rejects missing required fields", () => {
15
+ const s = object({
16
+ name: string(),
17
+ age: int(),
18
+ });
19
+ const result = s.safeParse({ name: "Alice" });
20
+ expect(result.success).toBe(false);
21
+ if (!result.success) {
22
+ expect(result.issues[0].code).toBe("required");
23
+ expect(result.issues[0].path).toEqual(["age"]);
24
+ }
25
+ });
26
+
27
+ it("strips unknown keys by default", () => {
28
+ const s = object({ name: string() });
29
+ const result = s.parse({ name: "Alice", extra: true });
30
+ expect(result).toEqual({ name: "Alice" });
31
+ });
32
+
33
+ it("rejects unknown keys when configured", () => {
34
+ const s = object({ name: string() }).unknownKeys("reject");
35
+ const result = s.safeParse({ name: "Alice", extra: true });
36
+ expect(result.success).toBe(false);
37
+ if (!result.success) {
38
+ expect(result.issues[0].code).toBe("unknown_key");
39
+ }
40
+ });
41
+
42
+ it("allows unknown keys when configured", () => {
43
+ const s = object({ name: string() }).unknownKeys("allow");
44
+ const result = s.parse({ name: "Alice", extra: true });
45
+ expect(result).toEqual({ name: "Alice", extra: true });
46
+ });
47
+
48
+ it("handles optional fields", () => {
49
+ const s = object({
50
+ name: string(),
51
+ nickname: optional(string()),
52
+ });
53
+ const result = s.parse({ name: "Alice" });
54
+ expect(result).toEqual({ name: "Alice" });
55
+ });
56
+
57
+ it("keeps object modifiers immutable", () => {
58
+ const base = object({ name: string() });
59
+ const rejected = base.unknownKeys("reject");
60
+
61
+ expect(base.parse({ name: "Alice", extra: true })).toEqual({
62
+ name: "Alice",
63
+ });
64
+ expect(rejected.safeParse({ name: "Alice", extra: true }).success).toBe(
65
+ false
66
+ );
67
+ });
68
+
69
+ it("handles defaults on fields", () => {
70
+ const s = object({
71
+ name: string(),
72
+ role: string().default("user"),
73
+ });
74
+ const result = s.parse({ name: "Alice" });
75
+ expect(result).toEqual({ name: "Alice", role: "user" });
76
+ });
77
+
78
+ it("does not overwrite present values with defaults", () => {
79
+ const s = object({
80
+ role: string().default("user"),
81
+ });
82
+ const result = s.parse({ role: "admin" });
83
+ expect(result).toEqual({ role: "admin" });
84
+ });
85
+
86
+ it("validates nested objects", () => {
87
+ const s = object({
88
+ user: object({
89
+ name: string(),
90
+ }),
91
+ });
92
+ const result = s.safeParse({ user: { name: 42 } });
93
+ expect(result.success).toBe(false);
94
+ if (!result.success) {
95
+ expect(result.issues[0].path).toEqual(["user", "name"]);
96
+ }
97
+ });
98
+
99
+ it("collects multiple nested issues with precise paths", () => {
100
+ const s = object({
101
+ user: object({
102
+ name: string().minLength(2),
103
+ age: int().min(18),
104
+ }),
105
+ active: bool(),
106
+ });
107
+
108
+ const result = s.safeParse({
109
+ user: { name: "", age: 12 },
110
+ active: "yes",
111
+ });
112
+
113
+ expect(result.success).toBe(false);
114
+ if (!result.success) {
115
+ expect(result.issues.map((issue) => issue.path)).toEqual(
116
+ expect.arrayContaining([
117
+ ["user", "name"],
118
+ ["user", "age"],
119
+ ["active"],
120
+ ])
121
+ );
122
+ }
123
+ });
124
+
125
+ it("treats inherited properties as absent", () => {
126
+ const base = { name: "Alice" };
127
+ const input = Object.create(base) as Record<string, unknown>;
128
+ input.age = 30;
129
+
130
+ const s = object({
131
+ name: string(),
132
+ age: int(),
133
+ });
134
+
135
+ const result = s.safeParse(input);
136
+ expect(result.success).toBe(false);
137
+ if (!result.success) {
138
+ expect(result.issues).toEqual(
139
+ expect.arrayContaining([
140
+ expect.objectContaining({
141
+ code: "required",
142
+ path: ["name"],
143
+ }),
144
+ ])
145
+ );
146
+ }
147
+ });
148
+
149
+ it("returns a detached parsed object", () => {
150
+ const s = object({
151
+ user: object({
152
+ name: string(),
153
+ }),
154
+ });
155
+
156
+ const input = { user: { name: "Alice" } };
157
+ const result = s.parse(input);
158
+
159
+ expect(result).toEqual(input);
160
+ expect(result).not.toBe(input);
161
+ expect(result.user).not.toBe(input.user);
162
+ });
163
+
164
+ it("preserves __proto__ as data when unknown keys are allowed", () => {
165
+ const s = object({ name: string() }).unknownKeys("allow");
166
+ const input = JSON.parse(
167
+ '{"name":"Alice","__proto__":{"polluted":"yes"}}'
168
+ ) as Record<string, unknown>;
169
+
170
+ const result = s.parse(input) as Record<string, unknown>;
171
+
172
+ expect(result.name).toBe("Alice");
173
+ expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
174
+ expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(true);
175
+ expect(
176
+ Object.getOwnPropertyDescriptor(result, "__proto__")?.value
177
+ ).toEqual({ polluted: "yes" });
178
+ expect(({} as Record<string, unknown>).polluted).toBeUndefined();
179
+ });
180
+
181
+ it("fails closed on cyclic input for recursive schemas", () => {
182
+ let node!: ReturnType<typeof object>;
183
+ const next = optional(new RefSchema("#/definitions/Node", () => node));
184
+ node = object({
185
+ value: string(),
186
+ next,
187
+ });
188
+
189
+ const input: Record<string, unknown> = { value: "root" };
190
+ input.next = input;
191
+
192
+ let result:
193
+ | ReturnType<typeof node.safeParse>
194
+ | undefined;
195
+
196
+ expect(() => {
197
+ result = node.safeParse(input);
198
+ }).not.toThrow();
199
+ expect(result?.success).toBe(false);
200
+ });
201
+
202
+ it("rejects non-objects", () => {
203
+ const s = object({ name: string() });
204
+ expect(s.safeParse("string").success).toBe(false);
205
+ expect(s.safeParse(null).success).toBe(false);
206
+ expect(s.safeParse([]).success).toBe(false);
207
+ });
208
+ });