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.
- package/CHANGELOG.md +44 -0
- package/README.md +370 -0
- package/VERSION +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/format/validators.d.ts +2 -0
- package/dist/format/validators.d.ts.map +1 -0
- package/dist/format/validators.js +57 -0
- package/dist/format/validators.js.map +1 -0
- package/dist/forms/index.d.ts +57 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +586 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/dist/infer.d.ts +8 -0
- package/dist/infer.d.ts.map +1 -0
- package/dist/infer.js +2 -0
- package/dist/infer.js.map +1 -0
- package/dist/interchange/document.d.ts +5 -0
- package/dist/interchange/document.d.ts.map +1 -0
- package/dist/interchange/document.js +12 -0
- package/dist/interchange/document.js.map +1 -0
- package/dist/interchange/exporter.d.ts +7 -0
- package/dist/interchange/exporter.d.ts.map +1 -0
- package/dist/interchange/exporter.js +7 -0
- package/dist/interchange/exporter.js.map +1 -0
- package/dist/interchange/importer.d.ts +4 -0
- package/dist/interchange/importer.d.ts.map +1 -0
- package/dist/interchange/importer.js +229 -0
- package/dist/interchange/importer.js.map +1 -0
- package/dist/issue-codes.d.ts +19 -0
- package/dist/issue-codes.d.ts.map +1 -0
- package/dist/issue-codes.js +18 -0
- package/dist/issue-codes.js.map +1 -0
- package/dist/parse/coerce.d.ts +16 -0
- package/dist/parse/coerce.d.ts.map +1 -0
- package/dist/parse/coerce.js +115 -0
- package/dist/parse/coerce.js.map +1 -0
- package/dist/parse/defaults.d.ts +7 -0
- package/dist/parse/defaults.d.ts.map +1 -0
- package/dist/parse/defaults.js +12 -0
- package/dist/parse/defaults.js.map +1 -0
- package/dist/parse/parser.d.ts +11 -0
- package/dist/parse/parser.d.ts.map +1 -0
- package/dist/parse/parser.js +13 -0
- package/dist/parse/parser.js.map +1 -0
- package/dist/schemas/any.d.ts +7 -0
- package/dist/schemas/any.d.ts.map +1 -0
- package/dist/schemas/any.js +12 -0
- package/dist/schemas/any.js.map +1 -0
- package/dist/schemas/array.d.ts +13 -0
- package/dist/schemas/array.d.ts.map +1 -0
- package/dist/schemas/array.js +73 -0
- package/dist/schemas/array.js.map +1 -0
- package/dist/schemas/base.d.ts +37 -0
- package/dist/schemas/base.d.ts.map +1 -0
- package/dist/schemas/base.js +285 -0
- package/dist/schemas/base.js.map +1 -0
- package/dist/schemas/bool.d.ts +8 -0
- package/dist/schemas/bool.d.ts.map +1 -0
- package/dist/schemas/bool.js +27 -0
- package/dist/schemas/bool.js.map +1 -0
- package/dist/schemas/enum.d.ts +9 -0
- package/dist/schemas/enum.d.ts.map +1 -0
- package/dist/schemas/enum.js +31 -0
- package/dist/schemas/enum.js.map +1 -0
- package/dist/schemas/index.d.ts +21 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +21 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/int.d.ts +32 -0
- package/dist/schemas/int.d.ts.map +1 -0
- package/dist/schemas/int.js +108 -0
- package/dist/schemas/int.js.map +1 -0
- package/dist/schemas/intersection.d.ts +16 -0
- package/dist/schemas/intersection.d.ts.map +1 -0
- package/dist/schemas/intersection.js +58 -0
- package/dist/schemas/intersection.js.map +1 -0
- package/dist/schemas/literal.d.ts +11 -0
- package/dist/schemas/literal.d.ts.map +1 -0
- package/dist/schemas/literal.js +28 -0
- package/dist/schemas/literal.js.map +1 -0
- package/dist/schemas/never.d.ts +7 -0
- package/dist/schemas/never.d.ts.map +1 -0
- package/dist/schemas/never.js +19 -0
- package/dist/schemas/never.js.map +1 -0
- package/dist/schemas/null.d.ts +7 -0
- package/dist/schemas/null.d.ts.map +1 -0
- package/dist/schemas/null.js +24 -0
- package/dist/schemas/null.js.map +1 -0
- package/dist/schemas/nullable.d.ts +10 -0
- package/dist/schemas/nullable.d.ts.map +1 -0
- package/dist/schemas/nullable.js +29 -0
- package/dist/schemas/nullable.js.map +1 -0
- package/dist/schemas/number.d.ts +27 -0
- package/dist/schemas/number.d.ts.map +1 -0
- package/dist/schemas/number.js +134 -0
- package/dist/schemas/number.js.map +1 -0
- package/dist/schemas/object.d.ts +28 -0
- package/dist/schemas/object.d.ts.map +1 -0
- package/dist/schemas/object.js +153 -0
- package/dist/schemas/object.js.map +1 -0
- package/dist/schemas/optional.d.ts +11 -0
- package/dist/schemas/optional.d.ts.map +1 -0
- package/dist/schemas/optional.js +39 -0
- package/dist/schemas/optional.js.map +1 -0
- package/dist/schemas/record.d.ts +9 -0
- package/dist/schemas/record.d.ts.map +1 -0
- package/dist/schemas/record.js +45 -0
- package/dist/schemas/record.js.map +1 -0
- package/dist/schemas/ref.d.ts +10 -0
- package/dist/schemas/ref.d.ts.map +1 -0
- package/dist/schemas/ref.js +30 -0
- package/dist/schemas/ref.js.map +1 -0
- package/dist/schemas/string.d.ts +29 -0
- package/dist/schemas/string.d.ts.map +1 -0
- package/dist/schemas/string.js +181 -0
- package/dist/schemas/string.js.map +1 -0
- package/dist/schemas/tuple.d.ts +14 -0
- package/dist/schemas/tuple.d.ts.map +1 -0
- package/dist/schemas/tuple.js +59 -0
- package/dist/schemas/tuple.js.map +1 -0
- package/dist/schemas/union.d.ts +9 -0
- package/dist/schemas/union.d.ts.map +1 -0
- package/dist/schemas/union.js +45 -0
- package/dist/schemas/union.js.map +1 -0
- package/dist/schemas/unknown.d.ts +7 -0
- package/dist/schemas/unknown.d.ts.map +1 -0
- package/dist/schemas/unknown.js +12 -0
- package/dist/schemas/unknown.js.map +1 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +12 -0
- package/dist/util.js.map +1 -0
- package/package.json +41 -0
- package/sdk/js/CHANGELOG.md +13 -0
- package/src/errors.ts +17 -0
- package/src/format/validators.ts +71 -0
- package/src/forms/index.ts +789 -0
- package/src/index.ts +285 -0
- package/src/infer.ts +12 -0
- package/src/interchange/document.ts +18 -0
- package/src/interchange/exporter.ts +12 -0
- package/src/interchange/importer.ts +285 -0
- package/src/issue-codes.ts +19 -0
- package/src/parse/coerce.ts +133 -0
- package/src/parse/defaults.ts +15 -0
- package/src/parse/parser.ts +19 -0
- package/src/schemas/any.ts +14 -0
- package/src/schemas/array.ts +83 -0
- package/src/schemas/base.ts +322 -0
- package/src/schemas/bool.ts +30 -0
- package/src/schemas/enum.ts +37 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/int.ts +129 -0
- package/src/schemas/intersection.ts +81 -0
- package/src/schemas/literal.ts +34 -0
- package/src/schemas/never.ts +21 -0
- package/src/schemas/null.ts +26 -0
- package/src/schemas/nullable.ts +36 -0
- package/src/schemas/number.ts +151 -0
- package/src/schemas/object.ts +203 -0
- package/src/schemas/optional.ts +49 -0
- package/src/schemas/record.ts +55 -0
- package/src/schemas/ref.ts +35 -0
- package/src/schemas/string.ts +192 -0
- package/src/schemas/tuple.ts +74 -0
- package/src/schemas/union.ts +53 -0
- package/src/schemas/unknown.ts +14 -0
- package/src/types.ts +239 -0
- package/src/util.ts +9 -0
- package/tests/conformance/runner.test.ts +28 -0
- package/tests/conformance/runner.ts +137 -0
- package/tests/forms.test.ts +146 -0
- package/tests/unit/coerce.test.ts +136 -0
- package/tests/unit/collections.test.ts +99 -0
- package/tests/unit/composition.test.ts +80 -0
- package/tests/unit/date-format.test.ts +18 -0
- package/tests/unit/default-mutation.test.ts +32 -0
- package/tests/unit/defaults.test.ts +49 -0
- package/tests/unit/errors.test.ts +53 -0
- package/tests/unit/export.test.ts +270 -0
- package/tests/unit/inference.test.ts +306 -0
- package/tests/unit/interchange.test.ts +191 -0
- package/tests/unit/number.test.ts +195 -0
- package/tests/unit/object.test.ts +208 -0
- package/tests/unit/parser.test.ts +151 -0
- package/tests/unit/primitives.test.ts +111 -0
- package/tests/unit/security-recursion.test.ts +105 -0
- package/tests/unit/security.test.ts +945 -0
- package/tests/unit/shared-ref-falsepos.test.ts +33 -0
- package/tests/unit/string-pattern-redos.test.ts +46 -0
- package/tests/unit/string.test.ts +147 -0
- package/tsconfig.json +21 -0
- 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
|
+
});
|