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.
Files changed (149) hide show
  1. package/.gitignore +232 -0
  2. package/CLAUDE.md +41 -0
  3. package/README.md +2 -0
  4. package/dist/cjs/common/guards.d.ts +5 -0
  5. package/dist/cjs/common/guards.d.ts.map +1 -0
  6. package/dist/cjs/common/guards.js +22 -0
  7. package/dist/cjs/common/guards.js.map +1 -0
  8. package/dist/cjs/common/index.d.ts +3 -0
  9. package/dist/cjs/common/index.d.ts.map +1 -0
  10. package/dist/cjs/common/index.js +19 -0
  11. package/dist/cjs/common/index.js.map +1 -0
  12. package/dist/cjs/common/padding.d.ts +2 -0
  13. package/dist/cjs/common/padding.d.ts.map +1 -0
  14. package/dist/cjs/common/padding.js +8 -0
  15. package/dist/cjs/common/padding.js.map +1 -0
  16. package/dist/cjs/common/types.d.ts +8 -0
  17. package/dist/cjs/common/types.d.ts.map +1 -0
  18. package/dist/cjs/common/types.js +3 -0
  19. package/dist/cjs/common/types.js.map +1 -0
  20. package/dist/cjs/index.d.ts +5 -0
  21. package/dist/cjs/index.d.ts.map +1 -0
  22. package/dist/cjs/index.js +21 -0
  23. package/dist/cjs/index.js.map +1 -0
  24. package/dist/cjs/inputs/index.d.ts +2 -0
  25. package/dist/cjs/inputs/index.d.ts.map +1 -0
  26. package/dist/cjs/inputs/index.js +18 -0
  27. package/dist/cjs/inputs/index.js.map +1 -0
  28. package/dist/cjs/inputs/json-schema.d.ts +4 -0
  29. package/dist/cjs/inputs/json-schema.d.ts.map +1 -0
  30. package/dist/cjs/inputs/json-schema.js +306 -0
  31. package/dist/cjs/inputs/json-schema.js.map +1 -0
  32. package/dist/cjs/phantom.d.ts +4 -0
  33. package/dist/cjs/phantom.d.ts.map +1 -0
  34. package/dist/cjs/phantom.js +5 -0
  35. package/dist/cjs/phantom.js.map +1 -0
  36. package/dist/cjs/shape-utils.d.ts +4 -0
  37. package/dist/cjs/shape-utils.d.ts.map +1 -0
  38. package/dist/cjs/shape-utils.js +43 -0
  39. package/dist/cjs/shape-utils.js.map +1 -0
  40. package/dist/cjs/shape.d.ts +169 -0
  41. package/dist/cjs/shape.d.ts.map +1 -0
  42. package/dist/cjs/shape.js +258 -0
  43. package/dist/cjs/shape.js.map +1 -0
  44. package/dist/cjs/translations/index.d.ts +4 -0
  45. package/dist/cjs/translations/index.d.ts.map +1 -0
  46. package/dist/cjs/translations/index.js +20 -0
  47. package/dist/cjs/translations/index.js.map +1 -0
  48. package/dist/cjs/translations/postgres.d.ts +3 -0
  49. package/dist/cjs/translations/postgres.d.ts.map +1 -0
  50. package/dist/cjs/translations/postgres.js +72 -0
  51. package/dist/cjs/translations/postgres.js.map +1 -0
  52. package/dist/cjs/translations/translation.d.ts +3 -0
  53. package/dist/cjs/translations/translation.d.ts.map +1 -0
  54. package/dist/cjs/translations/translation.js +3 -0
  55. package/dist/cjs/translations/translation.js.map +1 -0
  56. package/dist/cjs/translations/typescript.d.ts +3 -0
  57. package/dist/cjs/translations/typescript.d.ts.map +1 -0
  58. package/dist/cjs/translations/typescript.js +122 -0
  59. package/dist/cjs/translations/typescript.js.map +1 -0
  60. package/dist/cjs/validation.d.ts +10 -0
  61. package/dist/cjs/validation.d.ts.map +1 -0
  62. package/dist/cjs/validation.js +101 -0
  63. package/dist/cjs/validation.js.map +1 -0
  64. package/dist/esm/common/guards.d.ts +5 -0
  65. package/dist/esm/common/guards.d.ts.map +1 -0
  66. package/dist/esm/common/guards.js +22 -0
  67. package/dist/esm/common/guards.js.map +1 -0
  68. package/dist/esm/common/index.d.ts +3 -0
  69. package/dist/esm/common/index.d.ts.map +1 -0
  70. package/dist/esm/common/index.js +19 -0
  71. package/dist/esm/common/index.js.map +1 -0
  72. package/dist/esm/common/padding.d.ts +2 -0
  73. package/dist/esm/common/padding.d.ts.map +1 -0
  74. package/dist/esm/common/padding.js +8 -0
  75. package/dist/esm/common/padding.js.map +1 -0
  76. package/dist/esm/common/types.d.ts +8 -0
  77. package/dist/esm/common/types.d.ts.map +1 -0
  78. package/dist/esm/common/types.js +3 -0
  79. package/dist/esm/common/types.js.map +1 -0
  80. package/dist/esm/index.d.ts +5 -0
  81. package/dist/esm/index.d.ts.map +1 -0
  82. package/dist/esm/index.js +21 -0
  83. package/dist/esm/index.js.map +1 -0
  84. package/dist/esm/inputs/index.d.ts +2 -0
  85. package/dist/esm/inputs/index.d.ts.map +1 -0
  86. package/dist/esm/inputs/index.js +18 -0
  87. package/dist/esm/inputs/index.js.map +1 -0
  88. package/dist/esm/inputs/json-schema.d.ts +4 -0
  89. package/dist/esm/inputs/json-schema.d.ts.map +1 -0
  90. package/dist/esm/inputs/json-schema.js +306 -0
  91. package/dist/esm/inputs/json-schema.js.map +1 -0
  92. package/dist/esm/package.json +1 -0
  93. package/dist/esm/phantom.d.ts +4 -0
  94. package/dist/esm/phantom.d.ts.map +1 -0
  95. package/dist/esm/phantom.js +5 -0
  96. package/dist/esm/phantom.js.map +1 -0
  97. package/dist/esm/shape-utils.d.ts +4 -0
  98. package/dist/esm/shape-utils.d.ts.map +1 -0
  99. package/dist/esm/shape-utils.js +43 -0
  100. package/dist/esm/shape-utils.js.map +1 -0
  101. package/dist/esm/shape.d.ts +169 -0
  102. package/dist/esm/shape.d.ts.map +1 -0
  103. package/dist/esm/shape.js +258 -0
  104. package/dist/esm/shape.js.map +1 -0
  105. package/dist/esm/translations/index.d.ts +4 -0
  106. package/dist/esm/translations/index.d.ts.map +1 -0
  107. package/dist/esm/translations/index.js +20 -0
  108. package/dist/esm/translations/index.js.map +1 -0
  109. package/dist/esm/translations/postgres.d.ts +3 -0
  110. package/dist/esm/translations/postgres.d.ts.map +1 -0
  111. package/dist/esm/translations/postgres.js +72 -0
  112. package/dist/esm/translations/postgres.js.map +1 -0
  113. package/dist/esm/translations/translation.d.ts +3 -0
  114. package/dist/esm/translations/translation.d.ts.map +1 -0
  115. package/dist/esm/translations/translation.js +3 -0
  116. package/dist/esm/translations/translation.js.map +1 -0
  117. package/dist/esm/translations/typescript.d.ts +3 -0
  118. package/dist/esm/translations/typescript.d.ts.map +1 -0
  119. package/dist/esm/translations/typescript.js +122 -0
  120. package/dist/esm/translations/typescript.js.map +1 -0
  121. package/dist/esm/validation.d.ts +10 -0
  122. package/dist/esm/validation.d.ts.map +1 -0
  123. package/dist/esm/validation.js +101 -0
  124. package/dist/esm/validation.js.map +1 -0
  125. package/package.json +44 -0
  126. package/pnpm-lock.yaml +969 -0
  127. package/src/common/guards.ts +23 -0
  128. package/src/common/index.ts +2 -0
  129. package/src/common/padding.ts +6 -0
  130. package/src/common/types.ts +21 -0
  131. package/src/index.ts +4 -0
  132. package/src/inputs/index.ts +1 -0
  133. package/src/inputs/json-schema.test.ts +191 -0
  134. package/src/inputs/json-schema.ts +324 -0
  135. package/src/phantom.ts +3 -0
  136. package/src/samples/bank.sample.ts +25 -0
  137. package/src/shape-utils.ts +46 -0
  138. package/src/shape.ts +488 -0
  139. package/src/translations/index.ts +3 -0
  140. package/src/translations/postgres.test.ts +161 -0
  141. package/src/translations/postgres.ts +73 -0
  142. package/src/translations/translation.ts +3 -0
  143. package/src/translations/typescript.test.ts +61 -0
  144. package/src/translations/typescript.ts +142 -0
  145. package/src/validation.test.ts +179 -0
  146. package/src/validation.ts +135 -0
  147. package/tsconfig.cjs.json +10 -0
  148. package/tsconfig.esm.json +10 -0
  149. package/tsconfig.json +22 -0
@@ -0,0 +1,3 @@
1
+ import { Shape } from "../shape";
2
+
3
+ export type TranslationFunc<T extends any = any> = (shape: Shape) => T;
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/cjs",
5
+ "module": "commonjs",
6
+ "moduleResolution": "node10"
7
+ },
8
+ "include": ["./src/**/*.ts"],
9
+ "exclude": ["./src/**/*.test.ts", "./src/**/*.sample.ts"]
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/esm",
5
+ "module": "nodenext",
6
+ "target": "esnext"
7
+ },
8
+ "include": ["./src/**/*.ts"],
9
+ "exclude": ["./src/**/*.test.ts", "./src/**/*.sample.ts"]
10
+ }
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
+ }