shapedef 1.0.0
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/.gitignore +232 -0
- package/CLAUDE.md +41 -0
- package/README.md +2 -0
- package/dist/cjs/common/guards.d.ts +5 -0
- package/dist/cjs/common/guards.d.ts.map +1 -0
- package/dist/cjs/common/guards.js +22 -0
- package/dist/cjs/common/guards.js.map +1 -0
- package/dist/cjs/common/index.d.ts +3 -0
- package/dist/cjs/common/index.d.ts.map +1 -0
- package/dist/cjs/common/index.js +19 -0
- package/dist/cjs/common/index.js.map +1 -0
- package/dist/cjs/common/padding.d.ts +2 -0
- package/dist/cjs/common/padding.d.ts.map +1 -0
- package/dist/cjs/common/padding.js +8 -0
- package/dist/cjs/common/padding.js.map +1 -0
- package/dist/cjs/common/types.d.ts +8 -0
- package/dist/cjs/common/types.d.ts.map +1 -0
- package/dist/cjs/common/types.js +3 -0
- package/dist/cjs/common/types.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/inputs/index.d.ts +2 -0
- package/dist/cjs/inputs/index.d.ts.map +1 -0
- package/dist/cjs/inputs/index.js +18 -0
- package/dist/cjs/inputs/index.js.map +1 -0
- package/dist/cjs/inputs/json-schema.d.ts +4 -0
- package/dist/cjs/inputs/json-schema.d.ts.map +1 -0
- package/dist/cjs/inputs/json-schema.js +306 -0
- package/dist/cjs/inputs/json-schema.js.map +1 -0
- package/dist/cjs/phantom.d.ts +4 -0
- package/dist/cjs/phantom.d.ts.map +1 -0
- package/dist/cjs/phantom.js +5 -0
- package/dist/cjs/phantom.js.map +1 -0
- package/dist/cjs/shape-utils.d.ts +4 -0
- package/dist/cjs/shape-utils.d.ts.map +1 -0
- package/dist/cjs/shape-utils.js +43 -0
- package/dist/cjs/shape-utils.js.map +1 -0
- package/dist/cjs/shape.d.ts +169 -0
- package/dist/cjs/shape.d.ts.map +1 -0
- package/dist/cjs/shape.js +258 -0
- package/dist/cjs/shape.js.map +1 -0
- package/dist/cjs/translations/index.d.ts +4 -0
- package/dist/cjs/translations/index.d.ts.map +1 -0
- package/dist/cjs/translations/index.js +20 -0
- package/dist/cjs/translations/index.js.map +1 -0
- package/dist/cjs/translations/postgres.d.ts +3 -0
- package/dist/cjs/translations/postgres.d.ts.map +1 -0
- package/dist/cjs/translations/postgres.js +72 -0
- package/dist/cjs/translations/postgres.js.map +1 -0
- package/dist/cjs/translations/translation.d.ts +3 -0
- package/dist/cjs/translations/translation.d.ts.map +1 -0
- package/dist/cjs/translations/translation.js +3 -0
- package/dist/cjs/translations/translation.js.map +1 -0
- package/dist/cjs/translations/typescript.d.ts +3 -0
- package/dist/cjs/translations/typescript.d.ts.map +1 -0
- package/dist/cjs/translations/typescript.js +122 -0
- package/dist/cjs/translations/typescript.js.map +1 -0
- package/dist/cjs/validation.d.ts +10 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +101 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/esm/common/guards.d.ts +5 -0
- package/dist/esm/common/guards.d.ts.map +1 -0
- package/dist/esm/common/guards.js +22 -0
- package/dist/esm/common/guards.js.map +1 -0
- package/dist/esm/common/index.d.ts +3 -0
- package/dist/esm/common/index.d.ts.map +1 -0
- package/dist/esm/common/index.js +19 -0
- package/dist/esm/common/index.js.map +1 -0
- package/dist/esm/common/padding.d.ts +2 -0
- package/dist/esm/common/padding.d.ts.map +1 -0
- package/dist/esm/common/padding.js +8 -0
- package/dist/esm/common/padding.js.map +1 -0
- package/dist/esm/common/types.d.ts +8 -0
- package/dist/esm/common/types.d.ts.map +1 -0
- package/dist/esm/common/types.js +3 -0
- package/dist/esm/common/types.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/inputs/index.d.ts +2 -0
- package/dist/esm/inputs/index.d.ts.map +1 -0
- package/dist/esm/inputs/index.js +18 -0
- package/dist/esm/inputs/index.js.map +1 -0
- package/dist/esm/inputs/json-schema.d.ts +4 -0
- package/dist/esm/inputs/json-schema.d.ts.map +1 -0
- package/dist/esm/inputs/json-schema.js +306 -0
- package/dist/esm/inputs/json-schema.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/phantom.d.ts +4 -0
- package/dist/esm/phantom.d.ts.map +1 -0
- package/dist/esm/phantom.js +5 -0
- package/dist/esm/phantom.js.map +1 -0
- package/dist/esm/shape-utils.d.ts +4 -0
- package/dist/esm/shape-utils.d.ts.map +1 -0
- package/dist/esm/shape-utils.js +43 -0
- package/dist/esm/shape-utils.js.map +1 -0
- package/dist/esm/shape.d.ts +169 -0
- package/dist/esm/shape.d.ts.map +1 -0
- package/dist/esm/shape.js +258 -0
- package/dist/esm/shape.js.map +1 -0
- package/dist/esm/translations/index.d.ts +4 -0
- package/dist/esm/translations/index.d.ts.map +1 -0
- package/dist/esm/translations/index.js +20 -0
- package/dist/esm/translations/index.js.map +1 -0
- package/dist/esm/translations/postgres.d.ts +3 -0
- package/dist/esm/translations/postgres.d.ts.map +1 -0
- package/dist/esm/translations/postgres.js +72 -0
- package/dist/esm/translations/postgres.js.map +1 -0
- package/dist/esm/translations/translation.d.ts +3 -0
- package/dist/esm/translations/translation.d.ts.map +1 -0
- package/dist/esm/translations/translation.js +3 -0
- package/dist/esm/translations/translation.js.map +1 -0
- package/dist/esm/translations/typescript.d.ts +3 -0
- package/dist/esm/translations/typescript.d.ts.map +1 -0
- package/dist/esm/translations/typescript.js +122 -0
- package/dist/esm/translations/typescript.js.map +1 -0
- package/dist/esm/validation.d.ts +10 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +101 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +44 -0
- package/pnpm-lock.yaml +969 -0
- package/src/common/guards.ts +23 -0
- package/src/common/index.ts +2 -0
- package/src/common/padding.ts +6 -0
- package/src/common/types.ts +21 -0
- package/src/index.ts +4 -0
- package/src/inputs/index.ts +1 -0
- package/src/inputs/json-schema.test.ts +191 -0
- package/src/inputs/json-schema.ts +324 -0
- package/src/phantom.ts +3 -0
- package/src/samples/bank.sample.ts +25 -0
- package/src/shape-utils.ts +46 -0
- package/src/shape.ts +488 -0
- package/src/translations/index.ts +3 -0
- package/src/translations/postgres.test.ts +161 -0
- package/src/translations/postgres.ts +73 -0
- package/src/translations/translation.ts +3 -0
- package/src/translations/typescript.test.ts +61 -0
- package/src/translations/typescript.ts +142 -0
- package/src/validation.test.ts +179 -0
- package/src/validation.ts +135 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect } from "vitest";
|
|
2
|
+
import { annotate, shapes } from "../shape";
|
|
3
|
+
import { typescript } from "./typescript";
|
|
4
|
+
|
|
5
|
+
describe("typescript", (it) => {
|
|
6
|
+
it("translates primitive types", () => {
|
|
7
|
+
expect(typescript(shapes.int())).toBe("number");
|
|
8
|
+
expect(typescript(shapes.float())).toBe("number");
|
|
9
|
+
expect(typescript(shapes.number())).toBe("number");
|
|
10
|
+
expect(typescript(shapes.bool())).toBe("boolean");
|
|
11
|
+
expect(typescript(shapes.str())).toBe("string");
|
|
12
|
+
expect(typescript(shapes.date())).toBe("Date");
|
|
13
|
+
expect(typescript(shapes.nil())).toBe("null");
|
|
14
|
+
expect(typescript(shapes.unknown())).toBe("unknown");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("translates unions", () => {
|
|
18
|
+
expect(typescript(shapes.union(shapes.int(), shapes.bool()))).toBe(
|
|
19
|
+
"number | boolean",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("translates arrays", () => {
|
|
24
|
+
expect(typescript(shapes.array(shapes.number()))).toBe("Array<number>");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("translates tuples", () => {
|
|
28
|
+
expect(typescript(shapes.tuple(shapes.number(), shapes.number()))).toBe(
|
|
29
|
+
"[number, number]",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("translates anonymous mappings", () => {
|
|
34
|
+
expect(
|
|
35
|
+
typescript(
|
|
36
|
+
shapes.mapping({
|
|
37
|
+
name: shapes.str(),
|
|
38
|
+
age: annotate.optional(shapes.number()),
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
).toBe(["{", " name: string,", " age?: number", "}"].join("\n"));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("translates named mappings", () => {
|
|
45
|
+
expect(
|
|
46
|
+
typescript(
|
|
47
|
+
annotate.named(
|
|
48
|
+
shapes.mapping({
|
|
49
|
+
name: shapes.str(),
|
|
50
|
+
age: annotate.optional(shapes.number()),
|
|
51
|
+
}),
|
|
52
|
+
"Foo",
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
).toBe(
|
|
56
|
+
["export interface Foo {", " name: string,", " age?: number", "}"].join(
|
|
57
|
+
"\n",
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { mkpad } from "../common/padding";
|
|
2
|
+
import { Shape } from "../shape";
|
|
3
|
+
import { TranslationFunc } from "./translation";
|
|
4
|
+
|
|
5
|
+
type Context = {
|
|
6
|
+
padding: number;
|
|
7
|
+
depth: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const incDepth = (ctx: Context): Context => ({ ...ctx, depth: ctx.depth + 1 });
|
|
11
|
+
|
|
12
|
+
const maybeTypeAlias = (shape: Shape, ctx: Context, out: string): string => {
|
|
13
|
+
if (ctx.depth > 0) return out;
|
|
14
|
+
const name = shape.anno.name || shape.anno.title;
|
|
15
|
+
if (name && !name.includes(" ")) return `export type ${name} = ${out}`;
|
|
16
|
+
return out;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const toBatched = <T>(arr: T[], batchSize: number): Array<T[]> => {
|
|
20
|
+
const batches: Array<T[]> = [];
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < arr.length; i += batchSize) {
|
|
23
|
+
batches.push(arr.slice(i, i + batchSize));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return batches;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const createComments = (shape: Shape): string[] => {
|
|
30
|
+
const parts: string[] = [];
|
|
31
|
+
const description = (shape.anno.description || "").trim();
|
|
32
|
+
if (description.length > 0) {
|
|
33
|
+
const chars = Array.from(description.trim());
|
|
34
|
+
const batches = toBatched(chars, 80);
|
|
35
|
+
const lines = batches.map((x) => x.join(""));
|
|
36
|
+
parts.push(...lines);
|
|
37
|
+
}
|
|
38
|
+
if (typeof shape.anno.min === "number") {
|
|
39
|
+
parts.push(`min: ${shape.anno.min}`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof shape.anno.max === "number") {
|
|
42
|
+
parts.push(`max: ${shape.anno.max}`);
|
|
43
|
+
}
|
|
44
|
+
return parts;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ts = (shape: Shape, ctx: Context): string => {
|
|
48
|
+
switch (shape.type) {
|
|
49
|
+
case "vector":
|
|
50
|
+
return "number[]";
|
|
51
|
+
case "number":
|
|
52
|
+
return "number";
|
|
53
|
+
case "int":
|
|
54
|
+
return "number";
|
|
55
|
+
case "float":
|
|
56
|
+
return "number";
|
|
57
|
+
case "str":
|
|
58
|
+
return "string";
|
|
59
|
+
case "bool":
|
|
60
|
+
return "boolean";
|
|
61
|
+
case "date":
|
|
62
|
+
return "Date";
|
|
63
|
+
case "nil":
|
|
64
|
+
return "null";
|
|
65
|
+
case "unknown":
|
|
66
|
+
return "unknown";
|
|
67
|
+
case "literal-number":
|
|
68
|
+
return `${shape.value}`;
|
|
69
|
+
case "literal-int":
|
|
70
|
+
return `${shape.value}`;
|
|
71
|
+
case "literal-float":
|
|
72
|
+
return `${shape.value}`;
|
|
73
|
+
case "literal-str":
|
|
74
|
+
return `'${shape.value}'`;
|
|
75
|
+
case "literal-bool":
|
|
76
|
+
return `${shape.value}`;
|
|
77
|
+
case "ref":
|
|
78
|
+
return shape.name
|
|
79
|
+
.replaceAll("#/definitions/", "")
|
|
80
|
+
.replaceAll("#/$defs/", "")
|
|
81
|
+
.replaceAll("-", "_");
|
|
82
|
+
case "array":
|
|
83
|
+
return maybeTypeAlias(
|
|
84
|
+
shape,
|
|
85
|
+
ctx,
|
|
86
|
+
`Array<${ts(shape.item, incDepth(ctx))}>`,
|
|
87
|
+
);
|
|
88
|
+
case "tuple":
|
|
89
|
+
return `[${shape.tup.map((x) => ts(x, incDepth(ctx))).join(", ")}]`;
|
|
90
|
+
case "union":
|
|
91
|
+
return maybeTypeAlias(
|
|
92
|
+
shape,
|
|
93
|
+
ctx,
|
|
94
|
+
Array.from(new Set(shape.ofs.map((x) => ts(x, incDepth(ctx))))).join(
|
|
95
|
+
" | ",
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
case "mapping": {
|
|
99
|
+
const parts: string[] = [];
|
|
100
|
+
const members: string[] = [];
|
|
101
|
+
const name = shape.anno.name;
|
|
102
|
+
if (name) {
|
|
103
|
+
const replaced = name.replaceAll("-", "_");
|
|
104
|
+
parts.push(`export interface ${replaced} `);
|
|
105
|
+
}
|
|
106
|
+
parts.push("{");
|
|
107
|
+
parts.push("\n");
|
|
108
|
+
for (const [k, v] of Object.entries(shape.rec)) {
|
|
109
|
+
const member: string[] = [];
|
|
110
|
+
|
|
111
|
+
const comments = createComments(v);
|
|
112
|
+
if (comments.length > 0) {
|
|
113
|
+
member.push(mkpad(ctx.padding + 2) + "/**");
|
|
114
|
+
member.push("\n");
|
|
115
|
+
member.push(
|
|
116
|
+
comments
|
|
117
|
+
.map((cmt) => `${mkpad(ctx.padding + 2)}* ${cmt}`)
|
|
118
|
+
.join("\n"),
|
|
119
|
+
);
|
|
120
|
+
member.push("\n");
|
|
121
|
+
member.push(mkpad(ctx.padding + 2) + "**/");
|
|
122
|
+
member.push("\n");
|
|
123
|
+
}
|
|
124
|
+
member.push(mkpad(ctx.padding + 2));
|
|
125
|
+
member.push(k);
|
|
126
|
+
if (v.anno.optional) member.push("?");
|
|
127
|
+
member.push(": ");
|
|
128
|
+
member.push(ts(v, incDepth({ ...ctx, padding: ctx.padding + 2 })));
|
|
129
|
+
members.push(member.join(""));
|
|
130
|
+
}
|
|
131
|
+
parts.push(members.join(",\n"));
|
|
132
|
+
parts.push("\n");
|
|
133
|
+
parts.push(mkpad(ctx.padding));
|
|
134
|
+
parts.push("}");
|
|
135
|
+
return parts.join("");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const typescript: TranslationFunc<string> = (shape) => {
|
|
141
|
+
return ts(shape, { padding: 0, depth: 0 });
|
|
142
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, expect } from "vitest";
|
|
2
|
+
import { annotate, shapes } from "./shape";
|
|
3
|
+
import { validate } from "./validation";
|
|
4
|
+
|
|
5
|
+
describe("validate", (it) => {
|
|
6
|
+
it("validates str", () => {
|
|
7
|
+
expect(validate("hello", shapes.str())).toEqual({ ok: true });
|
|
8
|
+
expect(validate(42, shapes.str())).toMatchObject({ ok: false });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("validates bool", () => {
|
|
12
|
+
expect(validate(true, shapes.bool())).toEqual({ ok: true });
|
|
13
|
+
expect(validate(false, shapes.bool())).toEqual({ ok: true });
|
|
14
|
+
expect(validate("true", shapes.bool())).toMatchObject({ ok: false });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("validates int", () => {
|
|
18
|
+
expect(validate(1, shapes.int())).toEqual({ ok: true });
|
|
19
|
+
expect(validate(1.5, shapes.int())).toMatchObject({ ok: false });
|
|
20
|
+
expect(validate("1", shapes.int())).toMatchObject({ ok: false });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("validates float", () => {
|
|
24
|
+
expect(validate(1.5, shapes.float())).toEqual({ ok: true });
|
|
25
|
+
expect(validate(1, shapes.float())).toMatchObject({ ok: false });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("validates number", () => {
|
|
29
|
+
expect(validate(1, shapes.number())).toEqual({ ok: true });
|
|
30
|
+
expect(validate(1.5, shapes.number())).toEqual({ ok: true });
|
|
31
|
+
expect(validate("1", shapes.number())).toMatchObject({ ok: false });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("validates nil", () => {
|
|
35
|
+
expect(validate(null, shapes.nil())).toEqual({ ok: true });
|
|
36
|
+
expect(validate(undefined, shapes.nil())).toMatchObject({ ok: false });
|
|
37
|
+
expect(validate(0, shapes.nil())).toMatchObject({ ok: false });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("validates date", () => {
|
|
41
|
+
expect(validate(new Date(), shapes.date())).toEqual({ ok: true });
|
|
42
|
+
expect(validate("2024-01-01", shapes.date())).toMatchObject({ ok: false });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("validates unknown (always ok)", () => {
|
|
46
|
+
expect(validate(null, shapes.unknown())).toEqual({ ok: true });
|
|
47
|
+
expect(validate(42, shapes.unknown())).toEqual({ ok: true });
|
|
48
|
+
expect(validate("anything", shapes.unknown())).toEqual({ ok: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("validates literals", () => {
|
|
52
|
+
expect(validate("hello", shapes.literalStr("hello"))).toEqual({ ok: true });
|
|
53
|
+
expect(validate("world", shapes.literalStr("hello"))).toMatchObject({
|
|
54
|
+
ok: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(validate(true, shapes.literalBool(true))).toEqual({ ok: true });
|
|
58
|
+
expect(validate(false, shapes.literalBool(true))).toMatchObject({
|
|
59
|
+
ok: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(validate(1, shapes.literalInt(1))).toEqual({ ok: true });
|
|
63
|
+
expect(validate(2, shapes.literalInt(1))).toMatchObject({ ok: false });
|
|
64
|
+
|
|
65
|
+
expect(validate(1.5, shapes.literalFloat(1.5))).toEqual({ ok: true });
|
|
66
|
+
expect(validate(1.6, shapes.literalFloat(1.5))).toMatchObject({
|
|
67
|
+
ok: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(validate(42, shapes.literalNumber(42))).toEqual({ ok: true });
|
|
71
|
+
expect(validate(43, shapes.literalNumber(42))).toMatchObject({ ok: false });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("validates array", () => {
|
|
75
|
+
expect(validate([1, 2, 3], shapes.array(shapes.int()))).toEqual({
|
|
76
|
+
ok: true,
|
|
77
|
+
});
|
|
78
|
+
expect(validate([], shapes.array(shapes.str()))).toEqual({ ok: true });
|
|
79
|
+
expect(validate("not an array", shapes.array(shapes.str()))).toMatchObject({
|
|
80
|
+
ok: false,
|
|
81
|
+
});
|
|
82
|
+
expect(validate([1, "oops", 3], shapes.array(shapes.int()))).toMatchObject({
|
|
83
|
+
ok: false,
|
|
84
|
+
path: [1],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("validates tuple", () => {
|
|
89
|
+
expect(
|
|
90
|
+
validate([1, "hello"], shapes.tuple(shapes.int(), shapes.str())),
|
|
91
|
+
).toEqual({ ok: true });
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
validate([1], shapes.tuple(shapes.int(), shapes.str())),
|
|
95
|
+
).toMatchObject({ ok: false });
|
|
96
|
+
|
|
97
|
+
expect(
|
|
98
|
+
validate([1, 2], shapes.tuple(shapes.int(), shapes.str())),
|
|
99
|
+
).toMatchObject({ ok: false, path: [1] });
|
|
100
|
+
|
|
101
|
+
expect(validate("not a tuple", shapes.tuple(shapes.int()))).toMatchObject({
|
|
102
|
+
ok: false,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("validates vector", () => {
|
|
107
|
+
expect(validate([1, 2, 3], shapes.vector(3))).toEqual({ ok: true });
|
|
108
|
+
expect(validate([1, 2], shapes.vector(3))).toMatchObject({ ok: false });
|
|
109
|
+
expect(validate([1, "x", 3], shapes.vector(3))).toMatchObject({
|
|
110
|
+
ok: false,
|
|
111
|
+
});
|
|
112
|
+
expect(validate("not an array", shapes.vector(3))).toMatchObject({
|
|
113
|
+
ok: false,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("validates mapping", () => {
|
|
118
|
+
expect(
|
|
119
|
+
validate(
|
|
120
|
+
{ name: "Alice", age: 30 },
|
|
121
|
+
shapes.mapping({ name: shapes.str(), age: shapes.int() }),
|
|
122
|
+
),
|
|
123
|
+
).toEqual({ ok: true });
|
|
124
|
+
|
|
125
|
+
expect(
|
|
126
|
+
validate(
|
|
127
|
+
{ name: "Alice" },
|
|
128
|
+
shapes.mapping({ name: shapes.str(), age: shapes.int() }),
|
|
129
|
+
),
|
|
130
|
+
).toMatchObject({ ok: false, path: ["age"] });
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
validate(
|
|
134
|
+
{ name: 99, age: 30 },
|
|
135
|
+
shapes.mapping({ name: shapes.str(), age: shapes.int() }),
|
|
136
|
+
),
|
|
137
|
+
).toMatchObject({ ok: false, path: ["name"] });
|
|
138
|
+
|
|
139
|
+
expect(
|
|
140
|
+
validate("not an object", shapes.mapping({ name: shapes.str() })),
|
|
141
|
+
).toMatchObject({ ok: false });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("validates mapping with optional fields", () => {
|
|
145
|
+
const shape = shapes.mapping({
|
|
146
|
+
name: shapes.str(),
|
|
147
|
+
nickname: annotate.optional(shapes.str()),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(validate({ name: "Alice" }, shape)).toEqual({ ok: true });
|
|
151
|
+
expect(validate({ name: "Alice", nickname: "Al" }, shape)).toEqual({
|
|
152
|
+
ok: true,
|
|
153
|
+
});
|
|
154
|
+
expect(validate({ nickname: "Al" }, shape)).toMatchObject({ ok: false });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("validates union", () => {
|
|
158
|
+
const shape = shapes.union(shapes.str(), shapes.int());
|
|
159
|
+
expect(validate("hello", shape)).toEqual({ ok: true });
|
|
160
|
+
expect(validate(42, shape)).toEqual({ ok: true });
|
|
161
|
+
expect(validate(true, shape)).toMatchObject({ ok: false });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns error path for nested failures", () => {
|
|
165
|
+
const shape = shapes.mapping({
|
|
166
|
+
user: shapes.mapping({
|
|
167
|
+
age: shapes.int(),
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
const result = validate({ user: { age: "not a number" } }, shape);
|
|
171
|
+
expect(result).toMatchObject({ ok: false, path: ["user", "age"] });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns error for ref shapes", () => {
|
|
175
|
+
expect(validate("anything", shapes.ref("MyType"))).toMatchObject({
|
|
176
|
+
ok: false,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { has, isPlainObject } from "./common";
|
|
2
|
+
import { Shape } from "./shape";
|
|
3
|
+
|
|
4
|
+
export type ValidationResult =
|
|
5
|
+
| {
|
|
6
|
+
ok: true;
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
ok: false;
|
|
10
|
+
error: string;
|
|
11
|
+
path: PropertyKey[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ok = (): Extract<ValidationResult, { ok: true }> => ({ ok: true });
|
|
15
|
+
const err = (
|
|
16
|
+
error: string,
|
|
17
|
+
path: PropertyKey[],
|
|
18
|
+
): Extract<ValidationResult, { ok: false }> => ({
|
|
19
|
+
ok: false,
|
|
20
|
+
error,
|
|
21
|
+
path,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const maybe = (
|
|
25
|
+
result: boolean,
|
|
26
|
+
error: string,
|
|
27
|
+
path: PropertyKey[],
|
|
28
|
+
): ValidationResult => (result ? ok() : err(error, path));
|
|
29
|
+
|
|
30
|
+
export function validate(x: unknown, shape: Shape): ValidationResult {
|
|
31
|
+
function check(
|
|
32
|
+
x: unknown,
|
|
33
|
+
shape: Shape,
|
|
34
|
+
path: PropertyKey[],
|
|
35
|
+
): ValidationResult {
|
|
36
|
+
switch (shape.type) {
|
|
37
|
+
case "literal-bool":
|
|
38
|
+
case "literal-str":
|
|
39
|
+
case "literal-int":
|
|
40
|
+
case "literal-float":
|
|
41
|
+
case "literal-number":
|
|
42
|
+
return maybe(
|
|
43
|
+
x === shape.value,
|
|
44
|
+
`expected literal ${shape.value}`,
|
|
45
|
+
path,
|
|
46
|
+
);
|
|
47
|
+
case "str":
|
|
48
|
+
return maybe(typeof x === "string", "expected a string", path);
|
|
49
|
+
case "bool":
|
|
50
|
+
return maybe(typeof x === "boolean", "expected a boolean", path);
|
|
51
|
+
case "int":
|
|
52
|
+
return maybe(
|
|
53
|
+
typeof x === "number" && Number.isInteger(x),
|
|
54
|
+
"expected an integer",
|
|
55
|
+
path,
|
|
56
|
+
);
|
|
57
|
+
case "float":
|
|
58
|
+
return maybe(
|
|
59
|
+
typeof x === "number" && !Number.isInteger(x),
|
|
60
|
+
"expected a float",
|
|
61
|
+
path,
|
|
62
|
+
);
|
|
63
|
+
case "number":
|
|
64
|
+
return maybe(typeof x === "number", "expected a number", path);
|
|
65
|
+
case "nil":
|
|
66
|
+
return maybe(x === null, "expected null", path);
|
|
67
|
+
case "date":
|
|
68
|
+
return maybe(x instanceof Date, "expected a date", path);
|
|
69
|
+
case "unknown":
|
|
70
|
+
return ok();
|
|
71
|
+
case "array": {
|
|
72
|
+
if (!Array.isArray(x)) return err("expected an array", path);
|
|
73
|
+
for (let i = 0; i < x.length; i++) {
|
|
74
|
+
const item = x[i];
|
|
75
|
+
const result = check(item, shape.item, [...path, i]);
|
|
76
|
+
if (!result.ok) return result;
|
|
77
|
+
}
|
|
78
|
+
return ok();
|
|
79
|
+
}
|
|
80
|
+
case "mapping": {
|
|
81
|
+
if (!isPlainObject(x)) return err("expected a plain object", path);
|
|
82
|
+
for (const [k, v] of Object.entries(shape.rec)) {
|
|
83
|
+
if (!has(x, k) || typeof x[k] === "undefined") {
|
|
84
|
+
if (v.anno.optional || v.type === "unknown") continue;
|
|
85
|
+
return err(`expected property ${k} to exist`, [...path, k]);
|
|
86
|
+
}
|
|
87
|
+
const result = check(x[k], v, [...path, k]);
|
|
88
|
+
if (!result.ok) return result;
|
|
89
|
+
}
|
|
90
|
+
return ok();
|
|
91
|
+
}
|
|
92
|
+
case "ref":
|
|
93
|
+
return err(`cannot validate ref`, path);
|
|
94
|
+
case "tuple": {
|
|
95
|
+
if (!Array.isArray(x)) return err("expected a tuple / array", path);
|
|
96
|
+
if (x.length !== shape.tup.length)
|
|
97
|
+
return err(
|
|
98
|
+
`expected a tuple of length ${shape.tup.length} but got a tuple / array of length ${x.length}`,
|
|
99
|
+
path,
|
|
100
|
+
);
|
|
101
|
+
for (let i = 0; i < shape.tup.length; i++) {
|
|
102
|
+
const v = shape.tup[i]!;
|
|
103
|
+
const result = check(x[i], v, [...path, i]);
|
|
104
|
+
if (!result.ok) return result;
|
|
105
|
+
}
|
|
106
|
+
return ok();
|
|
107
|
+
}
|
|
108
|
+
case "vector": {
|
|
109
|
+
if (!Array.isArray(x)) return err(`expected an array (vector)`, path);
|
|
110
|
+
if (x.length !== shape.dims)
|
|
111
|
+
return err(
|
|
112
|
+
`expected a vector with dimension ${shape.dims} but got dimensions ${x.length}`,
|
|
113
|
+
path,
|
|
114
|
+
);
|
|
115
|
+
if (!x.every((v) => typeof v === "number"))
|
|
116
|
+
return err(
|
|
117
|
+
`expected a numeric vector but it contained other types`,
|
|
118
|
+
path,
|
|
119
|
+
);
|
|
120
|
+
return ok();
|
|
121
|
+
}
|
|
122
|
+
case "union": {
|
|
123
|
+
let lastResult: ValidationResult = ok();
|
|
124
|
+
for (const v of shape.ofs) {
|
|
125
|
+
const result = check(x, v, path);
|
|
126
|
+
if (result.ok) return ok();
|
|
127
|
+
lastResult = result;
|
|
128
|
+
}
|
|
129
|
+
return lastResult;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return check(x, shape, []);
|
|
135
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "./dist/esm",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"lib": ["esnext"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"exactOptionalPropertyTypes": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"noUncheckedSideEffectImports": true,
|
|
17
|
+
"moduleDetection": "force",
|
|
18
|
+
"skipLibCheck": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["./src/**/*.ts"],
|
|
21
|
+
"exclude": ["dist", "node_modules"]
|
|
22
|
+
}
|