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