shapecraft 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/CLAUDE.md +227 -0
- package/README.md +22 -0
- package/apps/cli/node_modules/.bin/prettier +21 -0
- package/apps/cli/node_modules/.bin/tsc +21 -0
- package/apps/cli/node_modules/.bin/tsserver +21 -0
- package/apps/cli/node_modules/.bin/tsx +21 -0
- package/apps/cli/node_modules/.bin/vitest +21 -0
- package/apps/cli/package.json +47 -0
- package/apps/cli/src/index.ts +98 -0
- package/apps/cli/tsconfig.cjs.json +10 -0
- package/apps/cli/tsconfig.esm.json +10 -0
- package/apps/cli/tsconfig.json +22 -0
- package/package.json +16 -0
- package/packages/core/node_modules/.bin/prettier +21 -0
- package/packages/core/node_modules/.bin/tsc +21 -0
- package/packages/core/node_modules/.bin/tsserver +21 -0
- package/packages/core/node_modules/.bin/tsx +21 -0
- package/packages/core/node_modules/.bin/vitest +21 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +44 -0
- package/packages/core/src/common/array.test.ts +19 -0
- package/packages/core/src/common/array.ts +15 -0
- package/packages/core/src/common/index.ts +5 -0
- package/packages/core/src/common/is.ts +23 -0
- package/packages/core/src/common/object.ts +35 -0
- package/packages/core/src/common/phantom.ts +1 -0
- package/packages/core/src/common/result.ts +43 -0
- package/packages/core/src/common/string.ts +28 -0
- package/packages/core/src/common/types.ts +34 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/shape/annotate.ts +139 -0
- package/packages/core/src/shape/annotation.ts +47 -0
- package/packages/core/src/shape/base.ts +71 -0
- package/packages/core/src/shape/builder.test.ts +728 -0
- package/packages/core/src/shape/builder.ts +475 -0
- package/packages/core/src/shape/error.ts +4 -0
- package/packages/core/src/shape/index.ts +3 -0
- package/packages/core/src/shape/number.ts +118 -0
- package/packages/core/src/shape/shape.test.ts +792 -0
- package/packages/core/src/shape/shape.ts +377 -0
- package/packages/core/src/shape/tags.ts +14 -0
- package/packages/core/src/shape/transforms/index.ts +3 -0
- package/packages/core/src/shape/transforms/json-schema/index.ts +2 -0
- package/packages/core/src/shape/transforms/json-schema/transform.test.ts +850 -0
- package/packages/core/src/shape/transforms/json-schema/transform.ts +882 -0
- package/packages/core/src/shape/transforms/json-schema/types.ts +132 -0
- package/packages/core/src/shape/transforms/sql/dialects/dialect.ts +89 -0
- package/packages/core/src/shape/transforms/sql/dialects/index.ts +14 -0
- package/packages/core/src/shape/transforms/sql/dialects/postgres.ts +392 -0
- package/packages/core/src/shape/transforms/sql/dialects/sqlite.ts +333 -0
- package/packages/core/src/shape/transforms/sql/from-sql.test.ts +704 -0
- package/packages/core/src/shape/transforms/sql/from-sql.ts +210 -0
- package/packages/core/src/shape/transforms/sql/index.ts +3 -0
- package/packages/core/src/shape/transforms/sql/options.ts +6 -0
- package/packages/core/src/shape/transforms/sql/parser/check-decoder.ts +457 -0
- package/packages/core/src/shape/transforms/sql/parser/create-domain.ts +105 -0
- package/packages/core/src/shape/transforms/sql/parser/create-table.ts +809 -0
- package/packages/core/src/shape/transforms/sql/parser/create-type.ts +91 -0
- package/packages/core/src/shape/transforms/sql/parser/cursor.ts +179 -0
- package/packages/core/src/shape/transforms/sql/parser/default-decoder.ts +129 -0
- package/packages/core/src/shape/transforms/sql/parser/lexer.ts +289 -0
- package/packages/core/src/shape/transforms/sql/parser/pg-types.ts +247 -0
- package/packages/core/src/shape/transforms/sql/parser/sqlite-types.ts +103 -0
- package/packages/core/src/shape/transforms/sql/parser/statements.ts +127 -0
- package/packages/core/src/shape/transforms/sql/parser/type-spec.ts +159 -0
- package/packages/core/src/shape/transforms/sql/transform.sqlite.test.ts +448 -0
- package/packages/core/src/shape/transforms/sql/transform.test.ts +880 -0
- package/packages/core/src/shape/transforms/sql/transform.ts +295 -0
- package/packages/core/src/shape/transforms/typescript/index.ts +1 -0
- package/packages/core/src/shape/transforms/typescript/transform.ts +211 -0
- package/packages/core/src/shape/tuple.test.ts +171 -0
- package/packages/core/src/shape/validate.ts +413 -0
- package/packages/core/tsconfig.cjs.json +11 -0
- package/packages/core/tsconfig.esm.json +10 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/samples/node_modules/.bin/prettier +21 -0
- package/packages/samples/node_modules/.bin/tsc +21 -0
- package/packages/samples/node_modules/.bin/tsserver +21 -0
- package/packages/samples/node_modules/.bin/tsx +21 -0
- package/packages/samples/node_modules/.bin/vitest +21 -0
- package/packages/samples/package.json +47 -0
- package/packages/samples/src/blog.ts +49 -0
- package/packages/samples/src/config.ts +50 -0
- package/packages/samples/src/ecommerce.ts +65 -0
- package/packages/samples/src/embeddings.ts +43 -0
- package/packages/samples/src/events.ts +52 -0
- package/packages/samples/src/geometry.ts +62 -0
- package/packages/samples/src/index.ts +9 -0
- package/packages/samples/src/relational.ts +17 -0
- package/packages/samples/src/tuples.ts +67 -0
- package/packages/samples/src/user.ts +9 -0
- package/packages/samples/tsconfig.cjs.json +11 -0
- package/packages/samples/tsconfig.esm.json +10 -0
- package/packages/samples/tsconfig.json +23 -0
- package/pnpm-workspace.yaml +3 -0
- package/test-data/json-schema/address.json +35 -0
- package/test-data/json-schema/array-of-things.json +36 -0
- package/test-data/json-schema/basic.json +21 -0
- package/test-data/json-schema/blog-post.json +29 -0
- package/test-data/json-schema/calendar.json +48 -0
- package/test-data/json-schema/complex-object-with-nested-properties.json +41 -0
- package/test-data/json-schema/ecommerce-complex.json +344 -0
- package/test-data/json-schema/ecommerce-system.json +27 -0
- package/test-data/json-schema/enumerated-values.json +11 -0
- package/test-data/json-schema/fstab-entry.json +92 -0
- package/test-data/json-schema/geographical-location.json +20 -0
- package/test-data/json-schema/health-record.json +41 -0
- package/test-data/json-schema/job-posting.json +33 -0
- package/test-data/json-schema/movie.json +35 -0
- package/test-data/json-schema/regular-expression-pattern.json +12 -0
- package/test-data/json-schema/user-profile.json +33 -0
- package/test-data/sql/ecommerce.sql +641 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { describe, expect } from "vitest";
|
|
2
|
+
import { R } from "../common";
|
|
3
|
+
import { B } from "./builder";
|
|
4
|
+
import { ValidationError } from "./error";
|
|
5
|
+
|
|
6
|
+
function expectToBe<Input, Value extends Input>(
|
|
7
|
+
input: Input,
|
|
8
|
+
value: Value,
|
|
9
|
+
): asserts input is Value {
|
|
10
|
+
return expect(input).toBe(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function expectOk<T>(
|
|
14
|
+
res: R.Result<T, ValidationError[]>,
|
|
15
|
+
): asserts res is R.ResultOk<T> {
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
throw new Error(`expected ok, got err: ${JSON.stringify(res.error)}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function expectErr<T>(
|
|
22
|
+
res: R.Result<T, ValidationError[]>,
|
|
23
|
+
): asserts res is R.ResultErr<ValidationError[]> {
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`expected err, got ok with value: ${JSON.stringify(res.value)}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("B scalar constructors", (it) => {
|
|
32
|
+
it("builds primitive shapes with the correct type tag", () => {
|
|
33
|
+
expectToBe(B.string().shape.type, "string");
|
|
34
|
+
expectToBe(B.boolean().shape.type, "boolean");
|
|
35
|
+
expectToBe(B.notDefined().shape.type, "undefined");
|
|
36
|
+
expectToBe(B.date().shape.type, "date");
|
|
37
|
+
expectToBe(B.nil().shape.type, "nil");
|
|
38
|
+
expectToBe(B.unknown().shape.type, "unknown");
|
|
39
|
+
expectToBe(B.binary().shape.type, "binary");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("exposes the shape type via the builder's `type` field", () => {
|
|
43
|
+
expectToBe(B.string().type, "string");
|
|
44
|
+
expectToBe(B.boolean().type, "boolean");
|
|
45
|
+
expectToBe(B.binary().type, "binary");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("marks shapes as shape values with empty default annotations", () => {
|
|
49
|
+
const b = B.string();
|
|
50
|
+
expect(b.shape._shape).toBe(true);
|
|
51
|
+
expect(b.shape.anno).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("attaches the matching number tag for each numeric builder", () => {
|
|
55
|
+
expectToBe(B.number().shape.tag, "number");
|
|
56
|
+
expectToBe(B.int().shape.tag, "int");
|
|
57
|
+
expectToBe(B.int8().shape.tag, "int8");
|
|
58
|
+
expectToBe(B.uint8().shape.tag, "uint8");
|
|
59
|
+
expectToBe(B.int16().shape.tag, "int16");
|
|
60
|
+
expectToBe(B.uint16().shape.tag, "uint16");
|
|
61
|
+
expectToBe(B.int32().shape.tag, "int32");
|
|
62
|
+
expectToBe(B.uint32().shape.tag, "uint32");
|
|
63
|
+
expectToBe(B.int64().shape.tag, "int64");
|
|
64
|
+
expectToBe(B.uint64().shape.tag, "uint64");
|
|
65
|
+
expectToBe(B.float().shape.tag, "float");
|
|
66
|
+
expectToBe(B.float32().shape.tag, "float32");
|
|
67
|
+
expectToBe(B.float64().shape.tag, "float64");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("B.literal", (it) => {
|
|
72
|
+
it("creates a literal string shape carrying its input value", () => {
|
|
73
|
+
const s = B.literal("foo").shape;
|
|
74
|
+
expectToBe(s.type, "string");
|
|
75
|
+
if (s.type !== "string") return;
|
|
76
|
+
expect(s.literal).toBe(true);
|
|
77
|
+
expect(s.input).toBe("foo");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("creates a literal number shape carrying its input value", () => {
|
|
81
|
+
const s = B.literal(123).shape;
|
|
82
|
+
expectToBe(s.type, "number");
|
|
83
|
+
if (s.type !== "number") return;
|
|
84
|
+
expect(s.literal).toBe(true);
|
|
85
|
+
expect(s.input).toBe(123);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("creates a literal boolean shape carrying its input value", () => {
|
|
89
|
+
const s = B.literal(true).shape;
|
|
90
|
+
expectToBe(s.type, "boolean");
|
|
91
|
+
if (s.type !== "boolean") return;
|
|
92
|
+
expect(s.literal).toBe(true);
|
|
93
|
+
expect(s.input).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("throws when given a non-literal-eligible value", () => {
|
|
97
|
+
expect(() => B.literal(null as any)).toThrow();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("B container constructors", (it) => {
|
|
102
|
+
it("array stores the item shape on input", () => {
|
|
103
|
+
const item = B.string();
|
|
104
|
+
const s = B.array(item).shape;
|
|
105
|
+
expectToBe(s.type, "array");
|
|
106
|
+
expect(s.input).toBe(item.shape);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("mapping stores the record of shapes on input", () => {
|
|
110
|
+
const a = B.number();
|
|
111
|
+
const b = B.string();
|
|
112
|
+
const s = B.mapping({ a, b }).shape;
|
|
113
|
+
expectToBe(s.type, "mapping");
|
|
114
|
+
expect(Object.keys(s.input).sort()).toEqual(["a", "b"]);
|
|
115
|
+
expect(s.input.a).toBe(a.shape);
|
|
116
|
+
expect(s.input.b).toBe(b.shape);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("record stores its key/value shapes as a tuple on input", () => {
|
|
120
|
+
const k = B.string();
|
|
121
|
+
const v = B.number();
|
|
122
|
+
const s = B.record(k, v).shape;
|
|
123
|
+
expectToBe(s.type, "record");
|
|
124
|
+
expect(s.input[0]).toBe(k.shape);
|
|
125
|
+
expect(s.input[1]).toBe(v.shape);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("vector defaults its format to a generic number shape", () => {
|
|
129
|
+
const s = B.vector(3).shape;
|
|
130
|
+
expectToBe(s.type, "vector");
|
|
131
|
+
expect(s.input.dims).toBe(3);
|
|
132
|
+
expectToBe(s.input.format.type, "number");
|
|
133
|
+
expectToBe(s.input.format.tag, "number");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("vector accepts an explicit numeric format", () => {
|
|
137
|
+
const s = B.vector(4, B.float32()).shape;
|
|
138
|
+
expectToBe(s.type, "vector");
|
|
139
|
+
expect(s.input.dims).toBe(4);
|
|
140
|
+
expectToBe(s.input.format.tag, "float32");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("B.union", (it) => {
|
|
145
|
+
it("accepts variadic arguments", () => {
|
|
146
|
+
const a = B.string();
|
|
147
|
+
const b = B.number();
|
|
148
|
+
const s = B.union(a, b).shape;
|
|
149
|
+
expectToBe(s.type, "union");
|
|
150
|
+
expect(s.input).toEqual([a.shape, b.shape]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("supports a literal-string discriminator across variants", () => {
|
|
154
|
+
const s = B.union(B.literal("a"), B.literal("b")).shape;
|
|
155
|
+
expectToBe(s.type, "union");
|
|
156
|
+
expect(s.input).toHaveLength(2);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("builder annotation methods", (it) => {
|
|
161
|
+
it("optional() returns a new builder with anno.optional=true", () => {
|
|
162
|
+
const base = B.string();
|
|
163
|
+
const opt = base.optional();
|
|
164
|
+
expect(opt.shape.anno.optional).toBe(true);
|
|
165
|
+
expect(base.shape.anno).toEqual({});
|
|
166
|
+
expect(opt).not.toBe(base);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("unique() returns a new builder with anno.unique=true", () => {
|
|
170
|
+
const base = B.string();
|
|
171
|
+
const uniq = base.unique();
|
|
172
|
+
expect(uniq.shape.anno.unique).toBe(true);
|
|
173
|
+
expect(base.shape.anno).toEqual({});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("uniqueItems() on array builder sets anno.uniqueItems=true", () => {
|
|
177
|
+
const base = B.array(B.string());
|
|
178
|
+
const uniq = base.uniqueItems();
|
|
179
|
+
expect(uniq.shape.anno.uniqueItems).toBe(true);
|
|
180
|
+
expect(base.shape.anno).toEqual({});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("primary() returns a new builder with anno.primary=true", () => {
|
|
184
|
+
const base = B.string();
|
|
185
|
+
const prim = base.primary();
|
|
186
|
+
expect(prim.shape.anno.primary).toBe(true);
|
|
187
|
+
expect(base.shape.anno).toEqual({});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("chains optional + unique + primary into the same anno", () => {
|
|
191
|
+
const b = B.string().optional().unique().primary();
|
|
192
|
+
expect(b.shape.anno.optional).toBe(true);
|
|
193
|
+
expect(b.shape.anno.unique).toBe(true);
|
|
194
|
+
expect(b.shape.anno.primary).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("preserves the underlying shape type after annotation", () => {
|
|
198
|
+
const item = B.string();
|
|
199
|
+
const arr = B.array(item).optional();
|
|
200
|
+
expectToBe(arr.shape.type, "array");
|
|
201
|
+
expect(arr.shape.input).toBe(item.shape);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("builder bound methods", (it) => {
|
|
206
|
+
it("min/max on a string builder set the annotation values", () => {
|
|
207
|
+
const b = B.string().min(2).max(4);
|
|
208
|
+
expect(b.shape.anno.min).toBe(2);
|
|
209
|
+
expect(b.shape.anno.max).toBe(4);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("min/max on a number builder set the annotation values", () => {
|
|
213
|
+
const b = B.number().min(0).max(10);
|
|
214
|
+
expect(b.shape.anno.min).toBe(0);
|
|
215
|
+
expect(b.shape.anno.max).toBe(10);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("min/max on an array builder set the annotation values", () => {
|
|
219
|
+
const b = B.array(B.string()).min(1).max(3);
|
|
220
|
+
expect(b.shape.anno.min).toBe(1);
|
|
221
|
+
expect(b.shape.anno.max).toBe(3);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("regex() stores a RegExp pattern on the string anno", () => {
|
|
225
|
+
const pat = /^[a-z]+$/;
|
|
226
|
+
const b = B.string().regex(pat);
|
|
227
|
+
expect(b.shape.anno.pattern).toBe(pat);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("regex() stores a string-source pattern on the string anno", () => {
|
|
231
|
+
const b = B.string().regex("^[A-Z]{3}$");
|
|
232
|
+
expect(b.shape.anno.pattern).toBe("^[A-Z]{3}$");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("builder.parse — scalars", (it) => {
|
|
237
|
+
it("accepts a string against a string builder", () => {
|
|
238
|
+
const res = B.string().parse("hello");
|
|
239
|
+
expectOk(res);
|
|
240
|
+
expect(res.value).toBe("hello");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("rejects a non-string against a string builder", () => {
|
|
244
|
+
const res = B.string().parse(42);
|
|
245
|
+
expectErr(res);
|
|
246
|
+
expect(res.error).toHaveLength(1);
|
|
247
|
+
expect(res.error[0]!.path).toEqual([]);
|
|
248
|
+
expect(res.error[0]!.message).toMatch(/string/);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("accepts boolean values against a boolean builder", () => {
|
|
252
|
+
expectOk(B.boolean().parse(true));
|
|
253
|
+
expectOk(B.boolean().parse(false));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("rejects a non-castable non-boolean against a boolean builder", () => {
|
|
257
|
+
const res = B.boolean().parse("not-a-bool");
|
|
258
|
+
expectErr(res);
|
|
259
|
+
expect(res.error).toHaveLength(1);
|
|
260
|
+
expect(res.error[0]!.message).toMatch(/boolean/);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("accepts null against a nil builder", () => {
|
|
264
|
+
expectOk(B.nil().parse(null));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("rejects a non-null value against a nil builder", () => {
|
|
268
|
+
const res = B.nil().parse(0);
|
|
269
|
+
expectErr(res);
|
|
270
|
+
expect(res.error).toHaveLength(1);
|
|
271
|
+
expect(res.error[0]!.message).toMatch(/nil/);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("accepts undefined against a notDefined builder", () => {
|
|
275
|
+
expectOk(B.notDefined().parse(undefined));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("rejects a defined value against a notDefined builder", () => {
|
|
279
|
+
const res = B.notDefined().parse(null);
|
|
280
|
+
expectErr(res);
|
|
281
|
+
expect(res.error).toHaveLength(1);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("accepts a Date against a date builder", () => {
|
|
285
|
+
expectOk(B.date().parse(new Date()));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("rejects a non-Date against a date builder", () => {
|
|
289
|
+
const res = B.date().parse(true);
|
|
290
|
+
expectErr(res);
|
|
291
|
+
expect(res.error).toHaveLength(1);
|
|
292
|
+
expect(res.error[0]!.message).toMatch(/date/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("accepts any value against an unknown builder", () => {
|
|
296
|
+
expectOk(B.unknown().parse(1));
|
|
297
|
+
expectOk(B.unknown().parse("x"));
|
|
298
|
+
expectOk(B.unknown().parse(null));
|
|
299
|
+
expectOk(B.unknown().parse({ a: 1 }));
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("builder.parse — numbers", (it) => {
|
|
304
|
+
it("accepts a number against a generic number builder", () => {
|
|
305
|
+
expectOk(B.number().parse(42));
|
|
306
|
+
expectOk(B.number().parse(-1.5));
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("rejects a non-numeric value against a number builder", () => {
|
|
310
|
+
const res = B.number().parse("foo");
|
|
311
|
+
expectErr(res);
|
|
312
|
+
expect(res.error).toHaveLength(1);
|
|
313
|
+
expect(res.error[0]!.message).toMatch(/number/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("auto-casts a numeric string against a number builder", () => {
|
|
317
|
+
const res = B.number().parse("42");
|
|
318
|
+
expectOk(res);
|
|
319
|
+
expect(res.value).toBe(42);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("accepts a value that fits the requested integer tag", () => {
|
|
323
|
+
expectOk(B.uint8().parse(200));
|
|
324
|
+
expectOk(B.int16().parse(-1000));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("rejects a value that exceeds the requested integer tag's range", () => {
|
|
328
|
+
const res = B.uint8().parse(0xffff);
|
|
329
|
+
expectErr(res);
|
|
330
|
+
expect(res.error).toHaveLength(1);
|
|
331
|
+
expect(res.error[0]!.message).toMatch(/format/);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("rejects a float when an integer tag is required", () => {
|
|
335
|
+
const res = B.uint8().parse(0.5);
|
|
336
|
+
expectErr(res);
|
|
337
|
+
expect(res.error).toHaveLength(1);
|
|
338
|
+
expect(res.error[0]!.message).toMatch(/format/);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("rejects an int against a float32 tag", () => {
|
|
342
|
+
const res = B.float32().parse(1);
|
|
343
|
+
expectErr(res);
|
|
344
|
+
expect(res.error).toHaveLength(1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("accepts a float that fits inside float32", () => {
|
|
348
|
+
expectOk(B.float32().parse(0.5));
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("builder.parse — literals", (it) => {
|
|
353
|
+
it("accepts the matching literal string", () => {
|
|
354
|
+
expectOk(B.literal("foo").parse("foo"));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("rejects a non-matching literal string", () => {
|
|
358
|
+
const res = B.literal("foo").parse("bar");
|
|
359
|
+
expectErr(res);
|
|
360
|
+
expect(res.error).toHaveLength(1);
|
|
361
|
+
expect(res.error[0]!.message).toMatch(/literal/);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("accepts the matching literal number", () => {
|
|
365
|
+
expectOk(B.literal(7).parse(7));
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("rejects a non-matching literal number", () => {
|
|
369
|
+
const res = B.literal(7).parse(8);
|
|
370
|
+
expectErr(res);
|
|
371
|
+
expect(res.error).toHaveLength(1);
|
|
372
|
+
expect(res.error[0]!.message).toMatch(/literal/);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("accepts the matching literal boolean", () => {
|
|
376
|
+
expectOk(B.literal(true).parse(true));
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("rejects a non-matching literal boolean", () => {
|
|
380
|
+
const res = B.literal(true).parse(false);
|
|
381
|
+
expectErr(res);
|
|
382
|
+
expect(res.error).toHaveLength(1);
|
|
383
|
+
expect(res.error[0]!.message).toMatch(/literal/);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("builder.parse — binary", (it) => {
|
|
388
|
+
it("accepts a Uint8Array against a binary builder", () => {
|
|
389
|
+
expectOk(B.binary().parse(new Uint8Array([1, 2, 3])));
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("accepts an array of uint8-fitting numbers against a binary builder", () => {
|
|
393
|
+
expectOk(B.binary().parse([0, 127, 255]));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("rejects an array containing a non-uint8 value against a binary builder", () => {
|
|
397
|
+
const res = B.binary().parse([0, 256]);
|
|
398
|
+
expectErr(res);
|
|
399
|
+
expect(res.error.length).toBeGreaterThan(0);
|
|
400
|
+
expect(res.error[0]!.path).toEqual([1]);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("rejects a non-array, non-Uint8Array against a binary builder", () => {
|
|
404
|
+
const res = B.binary().parse("not binary");
|
|
405
|
+
expectErr(res);
|
|
406
|
+
expect(res.error).toHaveLength(1);
|
|
407
|
+
expect(res.error[0]!.message).toMatch(/binary/);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("builder.parse — array", (it) => {
|
|
412
|
+
it("accepts an array whose items all match the item builder", () => {
|
|
413
|
+
expectOk(B.array(B.string()).parse(["a", "b"]));
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("accepts an empty array regardless of item builder", () => {
|
|
417
|
+
expectOk(B.array(B.string()).parse([]));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("rejects a non-array, non-castable input", () => {
|
|
421
|
+
const res = B.array(B.string()).parse(42);
|
|
422
|
+
expectErr(res);
|
|
423
|
+
expect(res.error).toHaveLength(1);
|
|
424
|
+
expect(res.error[0]!.message).toMatch(/array/);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("reports the index of an invalid item in the path", () => {
|
|
428
|
+
const res = B.array(B.string()).parse(["a", 1, "c"]);
|
|
429
|
+
expectErr(res);
|
|
430
|
+
expect(res.error.length).toBeGreaterThan(0);
|
|
431
|
+
expect(res.error[0]!.path).toEqual([1]);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("builder.parse — vector", (it) => {
|
|
436
|
+
it("accepts an array whose length matches dims and items fit the format", () => {
|
|
437
|
+
expectOk(B.vector(3, B.uint8()).parse([1, 2, 3]));
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("rejects a non-array input", () => {
|
|
441
|
+
const res = B.vector(3).parse(42);
|
|
442
|
+
expectErr(res);
|
|
443
|
+
expect(res.error).toHaveLength(1);
|
|
444
|
+
expect(res.error[0]!.message).toMatch(/vector/);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("rejects a length mismatch", () => {
|
|
448
|
+
const res = B.vector(3).parse([1, 2]);
|
|
449
|
+
expectErr(res);
|
|
450
|
+
expect(res.error).toHaveLength(1);
|
|
451
|
+
expect(res.error[0]!.message).toMatch(/dimensions/);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("reports the index of a format-mismatched item", () => {
|
|
455
|
+
const res = B.vector(3, B.uint8()).parse([1, 999, 3]);
|
|
456
|
+
expectErr(res);
|
|
457
|
+
expect(res.error.length).toBeGreaterThan(0);
|
|
458
|
+
expect(res.error[0]!.path).toEqual([1]);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("builder.parse — mapping", (it) => {
|
|
463
|
+
it("accepts an object where every required key validates", () => {
|
|
464
|
+
const b = B.mapping({ a: B.string(), b: B.number() });
|
|
465
|
+
expectOk(b.parse({ a: "x", b: 1 }));
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("rejects a non-object input", () => {
|
|
469
|
+
const b = B.mapping({ a: B.string() });
|
|
470
|
+
const res = b.parse(42);
|
|
471
|
+
expectErr(res);
|
|
472
|
+
expect(res.error).toHaveLength(1);
|
|
473
|
+
expect(res.error[0]!.message).toMatch(/mapping/);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("rejects null against a mapping builder", () => {
|
|
477
|
+
const b = B.mapping({ a: B.string() });
|
|
478
|
+
const res = b.parse(null);
|
|
479
|
+
expectErr(res);
|
|
480
|
+
expect(res.error).toHaveLength(1);
|
|
481
|
+
expect(res.error[0]!.message).toMatch(/mapping/);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("reports a missing required key with the key in the path", () => {
|
|
485
|
+
const b = B.mapping({ a: B.string(), b: B.number() });
|
|
486
|
+
const res = b.parse({ a: "x" });
|
|
487
|
+
expectErr(res);
|
|
488
|
+
expect(res.error).toHaveLength(1);
|
|
489
|
+
expect(res.error[0]!.path).toEqual(["b"]);
|
|
490
|
+
expect(res.error[0]!.message).toMatch(/Missing required key/);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("allows a missing optional key", () => {
|
|
494
|
+
const b = B.mapping({ a: B.string(), b: B.number().optional() });
|
|
495
|
+
expectOk(b.parse({ a: "x" }));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("reports value-shape errors with the key in the path", () => {
|
|
499
|
+
const b = B.mapping({ a: B.string(), b: B.number() });
|
|
500
|
+
const res = b.parse({ a: "x", b: true });
|
|
501
|
+
expectErr(res);
|
|
502
|
+
expect(res.error).toHaveLength(1);
|
|
503
|
+
expect(res.error[0]!.path).toEqual(["b"]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("walks the path into nested mappings", () => {
|
|
507
|
+
const b = B.mapping({
|
|
508
|
+
outer: B.mapping({ inner: B.number() }),
|
|
509
|
+
});
|
|
510
|
+
const res = b.parse({ outer: { inner: true } });
|
|
511
|
+
expectErr(res);
|
|
512
|
+
expect(res.error).toHaveLength(1);
|
|
513
|
+
expect(res.error[0]!.path).toEqual(["outer", "inner"]);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe("builder.parse — record", (it) => {
|
|
518
|
+
it("accepts an object whose keys and values all validate", () => {
|
|
519
|
+
const b = B.record(B.string(), B.number());
|
|
520
|
+
expectOk(b.parse({ a: 1, b: 2 }));
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("rejects a non-object input", () => {
|
|
524
|
+
const b = B.record(B.string(), B.number());
|
|
525
|
+
const res = b.parse(1);
|
|
526
|
+
expectErr(res);
|
|
527
|
+
expect(res.error).toHaveLength(1);
|
|
528
|
+
expect(res.error[0]!.message).toMatch(/record/);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("rejects null against a record builder", () => {
|
|
532
|
+
const b = B.record(B.string(), B.number());
|
|
533
|
+
const res = b.parse(null);
|
|
534
|
+
expectErr(res);
|
|
535
|
+
expect(res.error).toHaveLength(1);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("reports a value-shape error with the key in the path", () => {
|
|
539
|
+
const b = B.record(B.string(), B.number());
|
|
540
|
+
const res = b.parse({ a: 1, b: true });
|
|
541
|
+
expectErr(res);
|
|
542
|
+
expect(res.error.length).toBeGreaterThan(0);
|
|
543
|
+
expect(res.error[0]!.path).toEqual(["b"]);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("accepts an empty object", () => {
|
|
547
|
+
const b = B.record(B.string(), B.number());
|
|
548
|
+
expectOk(b.parse({}));
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
describe("builder.parse — union", (it) => {
|
|
553
|
+
it("accepts a value matching any variant", () => {
|
|
554
|
+
const b = B.union(B.string(), B.number());
|
|
555
|
+
expectOk(b.parse("hi"));
|
|
556
|
+
expectOk(b.parse(1));
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("rejects a value matching no variant and collects errors from each", () => {
|
|
560
|
+
const b = B.union(B.string(), B.number());
|
|
561
|
+
const res = b.parse(true);
|
|
562
|
+
expectErr(res);
|
|
563
|
+
expect(res.error.length).toBeGreaterThanOrEqual(2);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe("builder.parse — min/max", (it) => {
|
|
568
|
+
it("accepts a number within [min, max]", () => {
|
|
569
|
+
const b = B.number().min(0).max(10);
|
|
570
|
+
expectOk(b.parse(0));
|
|
571
|
+
expectOk(b.parse(5));
|
|
572
|
+
expectOk(b.parse(10));
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("rejects a number below min", () => {
|
|
576
|
+
const b = B.number().min(0);
|
|
577
|
+
const res = b.parse(-1);
|
|
578
|
+
expectErr(res);
|
|
579
|
+
expect(res.error).toHaveLength(1);
|
|
580
|
+
expect(res.error[0]!.message).toMatch(/>= 0/);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("rejects a number above max", () => {
|
|
584
|
+
const b = B.number().max(10);
|
|
585
|
+
const res = b.parse(11);
|
|
586
|
+
expectErr(res);
|
|
587
|
+
expect(res.error).toHaveLength(1);
|
|
588
|
+
expect(res.error[0]!.message).toMatch(/<= 10/);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("checks the number format before applying bounds", () => {
|
|
592
|
+
const b = B.number().min(0).max(10);
|
|
593
|
+
const res = b.parse("foo");
|
|
594
|
+
expectErr(res);
|
|
595
|
+
expect(res.error).toHaveLength(1);
|
|
596
|
+
expect(res.error[0]!.message).toMatch(/number/);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("accepts a string whose length is within [min, max]", () => {
|
|
600
|
+
const b = B.string().min(2).max(4);
|
|
601
|
+
expectOk(b.parse("ab"));
|
|
602
|
+
expectOk(b.parse("abc"));
|
|
603
|
+
expectOk(b.parse("abcd"));
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("rejects a string shorter than min", () => {
|
|
607
|
+
const b = B.string().min(2);
|
|
608
|
+
const res = b.parse("a");
|
|
609
|
+
expectErr(res);
|
|
610
|
+
expect(res.error).toHaveLength(1);
|
|
611
|
+
expect(res.error[0]!.message).toMatch(/string length/);
|
|
612
|
+
expect(res.error[0]!.message).toMatch(/>= 2/);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("rejects a string longer than max", () => {
|
|
616
|
+
const b = B.string().max(3);
|
|
617
|
+
const res = b.parse("abcd");
|
|
618
|
+
expectErr(res);
|
|
619
|
+
expect(res.error).toHaveLength(1);
|
|
620
|
+
expect(res.error[0]!.message).toMatch(/string length/);
|
|
621
|
+
expect(res.error[0]!.message).toMatch(/<= 3/);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("accepts an array whose length is within [min, max]", () => {
|
|
625
|
+
const b = B.array(B.number()).min(1).max(3);
|
|
626
|
+
expectOk(b.parse([1]));
|
|
627
|
+
expectOk(b.parse([1, 2]));
|
|
628
|
+
expectOk(b.parse([1, 2, 3]));
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("rejects an array shorter than min", () => {
|
|
632
|
+
const b = B.array(B.number()).min(2);
|
|
633
|
+
const res = b.parse([1]);
|
|
634
|
+
expectErr(res);
|
|
635
|
+
expect(res.error).toHaveLength(1);
|
|
636
|
+
expect(res.error[0]!.message).toMatch(/array length/);
|
|
637
|
+
expect(res.error[0]!.message).toMatch(/>= 2/);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("rejects an array longer than max", () => {
|
|
641
|
+
const b = B.array(B.number()).max(2);
|
|
642
|
+
const res = b.parse([1, 2, 3]);
|
|
643
|
+
expectErr(res);
|
|
644
|
+
expect(res.error).toHaveLength(1);
|
|
645
|
+
expect(res.error[0]!.message).toMatch(/array length/);
|
|
646
|
+
expect(res.error[0]!.message).toMatch(/<= 2/);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("reports bounds errors at the shape's path inside a nested mapping", () => {
|
|
650
|
+
const b = B.mapping({ name: B.string().min(3) });
|
|
651
|
+
const res = b.parse({ name: "no" });
|
|
652
|
+
expectErr(res);
|
|
653
|
+
expect(res.error).toHaveLength(1);
|
|
654
|
+
expect(res.error[0]!.path).toEqual(["name"]);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe("builder.parse — pattern", (it) => {
|
|
659
|
+
it("accepts a string matching a RegExp pattern", () => {
|
|
660
|
+
const b = B.string().regex(/^[a-z]+$/);
|
|
661
|
+
expectOk(b.parse("abc"));
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("rejects a string not matching a RegExp pattern", () => {
|
|
665
|
+
const b = B.string().regex(/^[a-z]+$/);
|
|
666
|
+
const res = b.parse("abc123");
|
|
667
|
+
expectErr(res);
|
|
668
|
+
expect(res.error).toHaveLength(1);
|
|
669
|
+
expect(res.error[0]!.message).toMatch(/pattern/);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("accepts a string matching a string-source pattern", () => {
|
|
673
|
+
const b = B.string().regex("^[A-Z]{3}$");
|
|
674
|
+
expectOk(b.parse("USD"));
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("rejects a string not matching a string-source pattern", () => {
|
|
678
|
+
const b = B.string().regex("^[A-Z]{3}$");
|
|
679
|
+
const res = b.parse("usd");
|
|
680
|
+
expectErr(res);
|
|
681
|
+
expect(res.error).toHaveLength(1);
|
|
682
|
+
expect(res.error[0]!.message).toMatch(/pattern/);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("checks type before pattern", () => {
|
|
686
|
+
const b = B.string().regex(/^[a-z]+$/);
|
|
687
|
+
const res = b.parse(5);
|
|
688
|
+
expectErr(res);
|
|
689
|
+
expect(res.error).toHaveLength(1);
|
|
690
|
+
expect(res.error[0]!.message).toMatch(/string/);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("reports pattern errors at the shape's path inside a nested mapping", () => {
|
|
694
|
+
const b = B.mapping({ slug: B.string().regex(/^[a-z-]+$/) });
|
|
695
|
+
const res = b.parse({ slug: "Bad Slug" });
|
|
696
|
+
expectErr(res);
|
|
697
|
+
expect(res.error).toHaveLength(1);
|
|
698
|
+
expect(res.error[0]!.path).toEqual(["slug"]);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it("composes with min/max — bounds error takes precedence", () => {
|
|
702
|
+
const b = B.string()
|
|
703
|
+
.min(3)
|
|
704
|
+
.regex(/^[a-z]+$/);
|
|
705
|
+
const res = b.parse("a");
|
|
706
|
+
expectErr(res);
|
|
707
|
+
expect(res.error).toHaveLength(1);
|
|
708
|
+
expect(res.error[0]!.message).toMatch(/string length/);
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe("builder.parse — paths", (it) => {
|
|
713
|
+
it("uses [] as the path for a top-level scalar mismatch", () => {
|
|
714
|
+
const res = B.string().parse(1);
|
|
715
|
+
expectErr(res);
|
|
716
|
+
expect(res.error[0]!.path).toEqual([]);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("threads indices and keys through nested arrays and mappings", () => {
|
|
720
|
+
const b = B.mapping({
|
|
721
|
+
items: B.array(B.mapping({ name: B.string() })),
|
|
722
|
+
});
|
|
723
|
+
const res = b.parse({ items: [{ name: "ok" }, { name: 5 }] });
|
|
724
|
+
expectErr(res);
|
|
725
|
+
expect(res.error).toHaveLength(1);
|
|
726
|
+
expect(res.error[0]!.path).toEqual(["items", 1, "name"]);
|
|
727
|
+
});
|
|
728
|
+
});
|