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,49 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { string, int, object, optional } from "../../src/index.js";
|
|
3
|
+
|
|
4
|
+
describe("Defaults", () => {
|
|
5
|
+
it("applies default when value is absent", () => {
|
|
6
|
+
const s = string().default("fallback");
|
|
7
|
+
expect(s.parse(undefined)).toBe("fallback");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("does not apply default when value is present", () => {
|
|
11
|
+
const s = string().default("fallback");
|
|
12
|
+
expect(s.parse("provided")).toBe("provided");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("validates the defaulted value", () => {
|
|
16
|
+
const s = string().minLength(5).default("hi");
|
|
17
|
+
// default "hi" is only 2 chars, should fail validation with default_invalid
|
|
18
|
+
const result = s.safeParse(undefined);
|
|
19
|
+
expect(result.success).toBe(false);
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
expect(result.issues[0].code).toBe("default_invalid");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("applies defaults in object fields", () => {
|
|
26
|
+
const s = object({
|
|
27
|
+
name: string(),
|
|
28
|
+
role: string().default("user"),
|
|
29
|
+
});
|
|
30
|
+
const result = s.parse({ name: "Alice" });
|
|
31
|
+
expect(result.role).toBe("user");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not override present object field with default", () => {
|
|
35
|
+
const s = object({
|
|
36
|
+
role: string().default("user"),
|
|
37
|
+
});
|
|
38
|
+
const result = s.parse({ role: "admin" });
|
|
39
|
+
expect(result.role).toBe("admin");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("default on optional field", () => {
|
|
43
|
+
const s = object({
|
|
44
|
+
count: optional(int().default(0)),
|
|
45
|
+
});
|
|
46
|
+
const result = s.parse({});
|
|
47
|
+
expect(result).toEqual({ count: 0 });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ValidationError } from "../../src/errors.js";
|
|
3
|
+
import type { ValidationIssue } from "../../src/types.js";
|
|
4
|
+
|
|
5
|
+
describe("ValidationError", () => {
|
|
6
|
+
it("constructs with issues and formats message", () => {
|
|
7
|
+
const issues: ValidationIssue[] = [
|
|
8
|
+
{
|
|
9
|
+
code: "invalid_type",
|
|
10
|
+
message: "Expected string, received number",
|
|
11
|
+
path: ["name"],
|
|
12
|
+
expected: "string",
|
|
13
|
+
received: "number",
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
const err = new ValidationError(issues);
|
|
17
|
+
expect(err).toBeInstanceOf(Error);
|
|
18
|
+
expect(err.name).toBe("ValidationError");
|
|
19
|
+
expect(err.issues).toBe(issues);
|
|
20
|
+
expect(err.message).toBe("[invalid_type] name: Expected string, received number");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("formats message for root path (empty path)", () => {
|
|
24
|
+
const issues: ValidationIssue[] = [
|
|
25
|
+
{
|
|
26
|
+
code: "invalid_type",
|
|
27
|
+
message: "Expected string",
|
|
28
|
+
path: [],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const err = new ValidationError(issues);
|
|
32
|
+
expect(err.message).toBe("[invalid_type] Expected string");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("formats multiple issues joined by newline", () => {
|
|
36
|
+
const issues: ValidationIssue[] = [
|
|
37
|
+
{ code: "invalid_type", message: "bad type", path: ["a"] },
|
|
38
|
+
{ code: "too_small", message: "too short", path: ["b", "c"] },
|
|
39
|
+
];
|
|
40
|
+
const err = new ValidationError(issues);
|
|
41
|
+
expect(err.message).toBe(
|
|
42
|
+
"[invalid_type] a: bad type\n[too_small] b.c: too short"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("formats nested path with dot notation", () => {
|
|
47
|
+
const issues: ValidationIssue[] = [
|
|
48
|
+
{ code: "required", message: "required", path: ["x", 0, "y"] },
|
|
49
|
+
];
|
|
50
|
+
const err = new ValidationError(issues);
|
|
51
|
+
expect(err.message).toBe("[required] x.0.y: required");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
any,
|
|
4
|
+
unknown,
|
|
5
|
+
never,
|
|
6
|
+
null_,
|
|
7
|
+
literal,
|
|
8
|
+
enum_,
|
|
9
|
+
string,
|
|
10
|
+
int,
|
|
11
|
+
bool,
|
|
12
|
+
array,
|
|
13
|
+
tuple,
|
|
14
|
+
object,
|
|
15
|
+
record,
|
|
16
|
+
union,
|
|
17
|
+
intersection,
|
|
18
|
+
optional,
|
|
19
|
+
nullable,
|
|
20
|
+
exportSchema,
|
|
21
|
+
parse,
|
|
22
|
+
safeParse,
|
|
23
|
+
} from "../../src/index.js";
|
|
24
|
+
import { RefSchema } from "../../src/schemas/ref.js";
|
|
25
|
+
import { NullableSchema } from "../../src/schemas/nullable.js";
|
|
26
|
+
import { OptionalSchema } from "../../src/schemas/optional.js";
|
|
27
|
+
import { StringSchema } from "../../src/schemas/string.js";
|
|
28
|
+
import { ABSENT } from "../../src/schemas/base.js";
|
|
29
|
+
|
|
30
|
+
describe("_toNode / export for all schema types", () => {
|
|
31
|
+
it("any", () => {
|
|
32
|
+
const doc = exportSchema(any());
|
|
33
|
+
expect(doc.root.kind).toBe("any");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("any with default", () => {
|
|
37
|
+
const doc = exportSchema(any().default(42));
|
|
38
|
+
expect(doc.root.kind).toBe("any");
|
|
39
|
+
expect(doc.root.default).toBe(42);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("unknown", () => {
|
|
43
|
+
const doc = exportSchema(unknown());
|
|
44
|
+
expect(doc.root.kind).toBe("unknown");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("unknown with default", () => {
|
|
48
|
+
const doc = exportSchema(unknown().default("x"));
|
|
49
|
+
expect(doc.root.kind).toBe("unknown");
|
|
50
|
+
expect(doc.root.default).toBe("x");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("never", () => {
|
|
54
|
+
const doc = exportSchema(never());
|
|
55
|
+
expect(doc.root.kind).toBe("never");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("null", () => {
|
|
59
|
+
const doc = exportSchema(null_());
|
|
60
|
+
expect(doc.root.kind).toBe("null");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("null with default", () => {
|
|
64
|
+
const doc = exportSchema(null_().default(null));
|
|
65
|
+
expect(doc.root.kind).toBe("null");
|
|
66
|
+
expect(doc.root.default).toBe(null);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("literal", () => {
|
|
70
|
+
const doc = exportSchema(literal("hello"));
|
|
71
|
+
expect(doc.root.kind).toBe("literal");
|
|
72
|
+
expect((doc.root as any).value).toBe("hello");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("literal with default", () => {
|
|
76
|
+
const doc = exportSchema(literal(42).default(42));
|
|
77
|
+
expect(doc.root.default).toBe(42);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("enum", () => {
|
|
81
|
+
const doc = exportSchema(enum_(["a", "b", 1]));
|
|
82
|
+
expect(doc.root.kind).toBe("enum");
|
|
83
|
+
expect((doc.root as any).values).toEqual(["a", "b", 1]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("enum with default", () => {
|
|
87
|
+
const doc = exportSchema(enum_(["x", "y"]).default("x"));
|
|
88
|
+
expect(doc.root.default).toBe("x");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("tuple", () => {
|
|
92
|
+
const doc = exportSchema(tuple([string(), int()]));
|
|
93
|
+
expect(doc.root.kind).toBe("tuple");
|
|
94
|
+
expect((doc.root as any).elements).toHaveLength(2);
|
|
95
|
+
expect((doc.root as any).elements[0].kind).toBe("string");
|
|
96
|
+
expect((doc.root as any).elements[1].kind).toBe("int");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("tuple with default", () => {
|
|
100
|
+
const doc = exportSchema(tuple([string()]).default(["hi"] as any));
|
|
101
|
+
expect(doc.root.default).toEqual(["hi"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("record", () => {
|
|
105
|
+
const doc = exportSchema(record(int()));
|
|
106
|
+
expect(doc.root.kind).toBe("record");
|
|
107
|
+
expect((doc.root as any).valueSchema.kind).toBe("int");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("record with default", () => {
|
|
111
|
+
const doc = exportSchema(record(int()).default({} as any));
|
|
112
|
+
expect(doc.root.default).toEqual({});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("union", () => {
|
|
116
|
+
const doc = exportSchema(union([string(), int()]));
|
|
117
|
+
expect(doc.root.kind).toBe("union");
|
|
118
|
+
expect((doc.root as any).variants).toHaveLength(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("union with default", () => {
|
|
122
|
+
const doc = exportSchema(union([string(), int()]).default("x" as any));
|
|
123
|
+
expect(doc.root.default).toBe("x");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("intersection", () => {
|
|
127
|
+
const doc = exportSchema(
|
|
128
|
+
intersection([object({ a: string() }), object({ b: int() })])
|
|
129
|
+
);
|
|
130
|
+
expect(doc.root.kind).toBe("intersection");
|
|
131
|
+
expect((doc.root as any).allOf).toHaveLength(2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("intersection with default", () => {
|
|
135
|
+
const doc = exportSchema(
|
|
136
|
+
intersection([object({ a: string() })]).default({ a: "x" } as any)
|
|
137
|
+
);
|
|
138
|
+
expect(doc.root.default).toEqual({ a: "x" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("optional", () => {
|
|
142
|
+
const doc = exportSchema(optional(string()));
|
|
143
|
+
expect(doc.root.kind).toBe("optional");
|
|
144
|
+
expect((doc.root as any).inner.kind).toBe("string");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("optional with default", () => {
|
|
148
|
+
const doc = exportSchema(optional(string().default("hi")));
|
|
149
|
+
expect(doc.root.kind).toBe("optional");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("nullable", () => {
|
|
153
|
+
const doc = exportSchema(nullable(string()));
|
|
154
|
+
expect(doc.root.kind).toBe("nullable");
|
|
155
|
+
expect((doc.root as any).inner.kind).toBe("string");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("nullable with default", () => {
|
|
159
|
+
const doc = exportSchema(nullable(string()).default(null as any));
|
|
160
|
+
expect(doc.root.default).toBe(null);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("ref", () => {
|
|
164
|
+
const ref = new RefSchema("#/definitions/User");
|
|
165
|
+
const node = ref._toNode();
|
|
166
|
+
expect(node.kind).toBe("ref");
|
|
167
|
+
expect((node as any).ref).toBe("#/definitions/User");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("ref _validate without resolver adds issue", () => {
|
|
171
|
+
const ref = new RefSchema("#/definitions/Missing");
|
|
172
|
+
const result = ref.safeParse("anything");
|
|
173
|
+
expect(result.success).toBe(false);
|
|
174
|
+
if (!result.success) {
|
|
175
|
+
expect(result.issues[0].code).toBe("unsupported_schema_kind");
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("ref _validate with resolver delegates", () => {
|
|
180
|
+
const ref = new RefSchema("#/definitions/Name", () => string());
|
|
181
|
+
expect(ref.parse("hello")).toBe("hello");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("Top-level parse/safeParse functions", () => {
|
|
186
|
+
it("parse delegates to schema.parse", () => {
|
|
187
|
+
expect(parse(string(), "hello")).toBe("hello");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("parse throws on failure", () => {
|
|
191
|
+
expect(() => parse(string(), 42)).toThrow();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("safeParse returns result", () => {
|
|
195
|
+
const r = safeParse(string(), "hi");
|
|
196
|
+
expect(r.success).toBe(true);
|
|
197
|
+
if (r.success) expect(r.data).toBe("hi");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("safeParse returns failure", () => {
|
|
201
|
+
const r = safeParse(string(), 42);
|
|
202
|
+
expect(r.success).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("base.ts edge cases", () => {
|
|
207
|
+
it("_getCoercionTarget returns 'unknown' by default", () => {
|
|
208
|
+
const s = any();
|
|
209
|
+
expect((s as any)._getCoercionTarget()).toBe("unknown");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("export throws in portable mode when not portable", () => {
|
|
213
|
+
const s = any();
|
|
214
|
+
(s as any)._isPortable = false;
|
|
215
|
+
expect(() => s.export("portable")).toThrow(
|
|
216
|
+
"Cannot export in portable mode"
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("export works in extended mode even if not portable", () => {
|
|
221
|
+
const s = any();
|
|
222
|
+
(s as any)._isPortable = false;
|
|
223
|
+
const doc = s.export("extended");
|
|
224
|
+
expect(doc.root.kind).toBe("any");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("nullable._validate direct calls", () => {
|
|
229
|
+
it("returns null when input is null", () => {
|
|
230
|
+
const s = new NullableSchema(new StringSchema());
|
|
231
|
+
const ctx = { path: [], issues: [] };
|
|
232
|
+
expect(s._validate(null, ctx)).toBe(null);
|
|
233
|
+
expect(ctx.issues).toHaveLength(0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("delegates to inner when input is not null", () => {
|
|
237
|
+
const s = new NullableSchema(new StringSchema());
|
|
238
|
+
const ctx = { path: [], issues: [] };
|
|
239
|
+
expect(s._validate("hello", ctx)).toBe("hello");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("inner validation failure propagates", () => {
|
|
243
|
+
const s = new NullableSchema(new StringSchema());
|
|
244
|
+
const ctx = { path: [], issues: [] };
|
|
245
|
+
s._validate(42, ctx);
|
|
246
|
+
expect(ctx.issues.length).toBeGreaterThan(0);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("optional._validate direct calls", () => {
|
|
251
|
+
it("returns undefined when input is undefined", () => {
|
|
252
|
+
const s = new OptionalSchema(new StringSchema());
|
|
253
|
+
const ctx = { path: [], issues: [] };
|
|
254
|
+
expect(s._validate(undefined, ctx)).toBe(undefined);
|
|
255
|
+
expect(ctx.issues).toHaveLength(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("returns undefined when input is ABSENT", () => {
|
|
259
|
+
const s = new OptionalSchema(new StringSchema());
|
|
260
|
+
const ctx = { path: [], issues: [] };
|
|
261
|
+
expect(s._validate(ABSENT, ctx)).toBe(undefined);
|
|
262
|
+
expect(ctx.issues).toHaveLength(0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("delegates to inner when input is present", () => {
|
|
266
|
+
const s = new OptionalSchema(new StringSchema());
|
|
267
|
+
const ctx = { path: [], issues: [] };
|
|
268
|
+
expect(s._validate("hello", ctx)).toBe("hello");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { describe, it, expect, expectTypeOf } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
object,
|
|
4
|
+
string,
|
|
5
|
+
int,
|
|
6
|
+
int64,
|
|
7
|
+
number,
|
|
8
|
+
bool,
|
|
9
|
+
null_,
|
|
10
|
+
array,
|
|
11
|
+
tuple,
|
|
12
|
+
record,
|
|
13
|
+
union,
|
|
14
|
+
intersection,
|
|
15
|
+
optional,
|
|
16
|
+
nullable,
|
|
17
|
+
literal,
|
|
18
|
+
enum_,
|
|
19
|
+
type Infer,
|
|
20
|
+
} from "../../src/index.js";
|
|
21
|
+
|
|
22
|
+
describe("Type Inference", () => {
|
|
23
|
+
describe("Infer<T> utility type", () => {
|
|
24
|
+
it("infers string", () => {
|
|
25
|
+
const schema = string();
|
|
26
|
+
type T = Infer<typeof schema>;
|
|
27
|
+
expectTypeOf<T>().toEqualTypeOf<string>();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("infers number", () => {
|
|
31
|
+
const schema = number();
|
|
32
|
+
type T = Infer<typeof schema>;
|
|
33
|
+
expectTypeOf<T>().toEqualTypeOf<number>();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("infers int as number", () => {
|
|
37
|
+
const schema = int();
|
|
38
|
+
type T = Infer<typeof schema>;
|
|
39
|
+
expectTypeOf<T>().toEqualTypeOf<number>();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("infers boolean", () => {
|
|
43
|
+
const schema = bool();
|
|
44
|
+
type T = Infer<typeof schema>;
|
|
45
|
+
expectTypeOf<T>().toEqualTypeOf<boolean>();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("infers null", () => {
|
|
49
|
+
const schema = null_();
|
|
50
|
+
type T = Infer<typeof schema>;
|
|
51
|
+
expectTypeOf<T>().toEqualTypeOf<null>();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("infers literal string", () => {
|
|
55
|
+
const schema = literal("hello");
|
|
56
|
+
type T = Infer<typeof schema>;
|
|
57
|
+
expectTypeOf<T>().toEqualTypeOf<"hello">();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("infers literal number", () => {
|
|
61
|
+
const schema = literal(42);
|
|
62
|
+
type T = Infer<typeof schema>;
|
|
63
|
+
expectTypeOf<T>().toEqualTypeOf<42>();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("Object type inference", () => {
|
|
68
|
+
it("infers flat object shape", () => {
|
|
69
|
+
const schema = object({
|
|
70
|
+
name: string(),
|
|
71
|
+
age: int(),
|
|
72
|
+
});
|
|
73
|
+
type T = Infer<typeof schema>;
|
|
74
|
+
expectTypeOf<T>().toEqualTypeOf<{ name: string; age: number }>();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("infers object with optional fields", () => {
|
|
78
|
+
const schema = object({
|
|
79
|
+
name: string(),
|
|
80
|
+
nick: optional(string()),
|
|
81
|
+
});
|
|
82
|
+
type T = Infer<typeof schema>;
|
|
83
|
+
expectTypeOf<T>().toEqualTypeOf<{
|
|
84
|
+
name: string;
|
|
85
|
+
nick?: string | undefined;
|
|
86
|
+
}>();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("infers object with nullable fields", () => {
|
|
90
|
+
const schema = object({
|
|
91
|
+
name: string(),
|
|
92
|
+
bio: nullable(string()),
|
|
93
|
+
});
|
|
94
|
+
type T = Infer<typeof schema>;
|
|
95
|
+
expectTypeOf<T>().toEqualTypeOf<{
|
|
96
|
+
name: string;
|
|
97
|
+
bio: string | null;
|
|
98
|
+
}>();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("infers nested objects", () => {
|
|
102
|
+
const schema = object({
|
|
103
|
+
user: object({
|
|
104
|
+
name: string(),
|
|
105
|
+
age: int(),
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
type T = Infer<typeof schema>;
|
|
109
|
+
expectTypeOf<T>().toEqualTypeOf<{
|
|
110
|
+
user: { name: string; age: number };
|
|
111
|
+
}>();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("parse returns typed value, not Record<string, unknown>", () => {
|
|
115
|
+
const schema = object({
|
|
116
|
+
a: string(),
|
|
117
|
+
});
|
|
118
|
+
const result = schema.parse({ a: "hello" });
|
|
119
|
+
expectTypeOf(result).toEqualTypeOf<{ a: string }>();
|
|
120
|
+
|
|
121
|
+
// This should be accessible:
|
|
122
|
+
expectTypeOf(result.a).toEqualTypeOf<string>();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("safeParse data is typed on success", () => {
|
|
126
|
+
const schema = object({
|
|
127
|
+
a: string(),
|
|
128
|
+
b: int(),
|
|
129
|
+
});
|
|
130
|
+
const result = schema.safeParse({ a: "hello", b: 1 });
|
|
131
|
+
if (result.success) {
|
|
132
|
+
expectTypeOf(result.data).toEqualTypeOf<{
|
|
133
|
+
a: string;
|
|
134
|
+
b: number;
|
|
135
|
+
}>();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("rejects accessing unknown properties at type level", () => {
|
|
140
|
+
const schema = object({ a: string() });
|
|
141
|
+
type T = Infer<typeof schema>;
|
|
142
|
+
// T should have 'a' but not 'b'
|
|
143
|
+
expectTypeOf<T>().toHaveProperty("a");
|
|
144
|
+
expectTypeOf<T>().not.toHaveProperty("b");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("Array type inference", () => {
|
|
149
|
+
it("infers array of strings", () => {
|
|
150
|
+
const schema = array(string());
|
|
151
|
+
type T = Infer<typeof schema>;
|
|
152
|
+
expectTypeOf<T>().toEqualTypeOf<string[]>();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("infers array of objects", () => {
|
|
156
|
+
const schema = array(
|
|
157
|
+
object({
|
|
158
|
+
id: int64(),
|
|
159
|
+
name: string(),
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
type T = Infer<typeof schema>;
|
|
163
|
+
expectTypeOf<T>().toEqualTypeOf<
|
|
164
|
+
{ id: number; name: string }[]
|
|
165
|
+
>();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("Tuple type inference", () => {
|
|
170
|
+
it("infers tuple element types", () => {
|
|
171
|
+
const schema = tuple([string(), int(), bool()]);
|
|
172
|
+
type T = Infer<typeof schema>;
|
|
173
|
+
expectTypeOf<T>().toEqualTypeOf<[string, number, boolean]>();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("Record type inference", () => {
|
|
178
|
+
it("infers record value type", () => {
|
|
179
|
+
const schema = record(number());
|
|
180
|
+
type T = Infer<typeof schema>;
|
|
181
|
+
expectTypeOf<T>().toEqualTypeOf<Record<string, number>>();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("Union type inference", () => {
|
|
186
|
+
it("infers union of types", () => {
|
|
187
|
+
const schema = union([string(), int()]);
|
|
188
|
+
type T = Infer<typeof schema>;
|
|
189
|
+
expectTypeOf<T>().toEqualTypeOf<string | number>();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("infers union of literals", () => {
|
|
193
|
+
const schema = union([literal("a"), literal("b")]);
|
|
194
|
+
type T = Infer<typeof schema>;
|
|
195
|
+
expectTypeOf<T>().toEqualTypeOf<"a" | "b">();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("Intersection type inference", () => {
|
|
200
|
+
it("infers intersection of object types", () => {
|
|
201
|
+
const schema = intersection([
|
|
202
|
+
object({ name: string() }),
|
|
203
|
+
object({ age: int() }),
|
|
204
|
+
]);
|
|
205
|
+
type T = Infer<typeof schema>;
|
|
206
|
+
expectTypeOf<T>().toEqualTypeOf<{
|
|
207
|
+
name: string;
|
|
208
|
+
age: number;
|
|
209
|
+
}>();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("Enum type inference", () => {
|
|
214
|
+
it("infers enum values as union", () => {
|
|
215
|
+
const schema = enum_(["free", "pro", "enterprise"] as const);
|
|
216
|
+
type T = Infer<typeof schema>;
|
|
217
|
+
expectTypeOf<T>().toEqualTypeOf<"free" | "pro" | "enterprise">();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("Modifier type inference", () => {
|
|
222
|
+
it("optional adds undefined", () => {
|
|
223
|
+
const schema = optional(string());
|
|
224
|
+
type T = Infer<typeof schema>;
|
|
225
|
+
expectTypeOf<T>().toEqualTypeOf<string | undefined>();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("nullable adds null", () => {
|
|
229
|
+
const schema = nullable(string());
|
|
230
|
+
type T = Infer<typeof schema>;
|
|
231
|
+
expectTypeOf<T>().toEqualTypeOf<string | null>();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("optional nullable adds both", () => {
|
|
235
|
+
const schema = optional(nullable(string()));
|
|
236
|
+
type T = Infer<typeof schema>;
|
|
237
|
+
expectTypeOf<T>().toEqualTypeOf<string | null | undefined>();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("Method chaining preserves types", () => {
|
|
242
|
+
it("string with constraints stays string", () => {
|
|
243
|
+
const schema = string().minLength(1).maxLength(100).format("email");
|
|
244
|
+
type T = Infer<typeof schema>;
|
|
245
|
+
expectTypeOf<T>().toEqualTypeOf<string>();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("int with constraints stays number", () => {
|
|
249
|
+
const schema = int().min(0).max(100);
|
|
250
|
+
type T = Infer<typeof schema>;
|
|
251
|
+
expectTypeOf<T>().toEqualTypeOf<number>();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("array with constraints preserves element type", () => {
|
|
255
|
+
const schema = array(string()).minItems(1).maxItems(10);
|
|
256
|
+
type T = Infer<typeof schema>;
|
|
257
|
+
expectTypeOf<T>().toEqualTypeOf<string[]>();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("Runtime behavior unchanged", () => {
|
|
262
|
+
it("object parse still works correctly", () => {
|
|
263
|
+
const schema = object({ a: string() });
|
|
264
|
+
const result = schema.parse({ a: "hello" });
|
|
265
|
+
expect(result).toEqual({ a: "hello" });
|
|
266
|
+
expect(result.a).toBe("hello");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("object safeParse still works correctly", () => {
|
|
270
|
+
const schema = object({ a: string(), b: int() });
|
|
271
|
+
const result = schema.safeParse({ a: "hello", b: 42 });
|
|
272
|
+
expect(result.success).toBe(true);
|
|
273
|
+
if (result.success) {
|
|
274
|
+
expect(result.data.a).toBe("hello");
|
|
275
|
+
expect(result.data.b).toBe(42);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("object safeParse strips unknown properties by default", () => {
|
|
280
|
+
const schema = object({ a: string() });
|
|
281
|
+
const result = schema.safeParse({ a: "hello", b: "extra" });
|
|
282
|
+
expect(result.success).toBe(true);
|
|
283
|
+
if (result.success) {
|
|
284
|
+
expect(result.data).toEqual({ a: "hello" });
|
|
285
|
+
expect("b" in result.data).toBe(false);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("object safeParse rejects unknown properties when unknownKeys('reject')", () => {
|
|
290
|
+
const schema = object({ a: string() }).unknownKeys("reject");
|
|
291
|
+
const result = schema.safeParse({ a: "hello", b: "extra" });
|
|
292
|
+
expect(result.success).toBe(false);
|
|
293
|
+
if (!result.success) {
|
|
294
|
+
expect(result.issues[0].code).toBe("unknown_key");
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("typed parse result correctly narrows - accessing known property is ok", () => {
|
|
299
|
+
const schema = object({ a: string() });
|
|
300
|
+
const res = schema.parse({ a: "a" });
|
|
301
|
+
// res.a should work fine at runtime and type level
|
|
302
|
+
expect(res.a).toBe("a");
|
|
303
|
+
expectTypeOf(res.a).toEqualTypeOf<string>();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|