shapedef 1.0.21 → 1.0.23
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/dist/cjs/inputs/json-schema.d.ts.map +1 -1
- package/dist/cjs/inputs/json-schema.js +8 -0
- package/dist/cjs/inputs/json-schema.js.map +1 -1
- package/dist/cjs/shape.d.ts +5 -1
- package/dist/cjs/shape.d.ts.map +1 -1
- package/dist/cjs/shape.js +122 -0
- package/dist/cjs/shape.js.map +1 -1
- package/dist/cjs/translations/postgres.d.ts.map +1 -1
- package/dist/cjs/translations/postgres.js +2 -0
- package/dist/cjs/translations/postgres.js.map +1 -1
- package/dist/cjs/translations/typescript.d.ts.map +1 -1
- package/dist/cjs/translations/typescript.js +2 -0
- package/dist/cjs/translations/typescript.js.map +1 -1
- package/dist/cjs/validation.d.ts.map +1 -1
- package/dist/cjs/validation.js +2 -0
- package/dist/cjs/validation.js.map +1 -1
- package/dist/esm/inputs/json-schema.d.ts.map +1 -1
- package/dist/esm/inputs/json-schema.js +8 -0
- package/dist/esm/inputs/json-schema.js.map +1 -1
- package/dist/esm/shape.d.ts +5 -1
- package/dist/esm/shape.d.ts.map +1 -1
- package/dist/esm/shape.js +122 -0
- package/dist/esm/shape.js.map +1 -1
- package/dist/esm/translations/postgres.d.ts.map +1 -1
- package/dist/esm/translations/postgres.js +2 -0
- package/dist/esm/translations/postgres.js.map +1 -1
- package/dist/esm/translations/typescript.d.ts.map +1 -1
- package/dist/esm/translations/typescript.js +2 -0
- package/dist/esm/translations/typescript.js.map +1 -1
- package/dist/esm/validation.d.ts.map +1 -1
- package/dist/esm/validation.js +2 -0
- package/dist/esm/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/inputs/json-schema.ts +8 -0
- package/src/shape.coerce.test.ts +285 -0
- package/src/shape.ts +123 -0
- package/src/translations/postgres.ts +2 -0
- package/src/translations/typescript.test.ts +1 -0
- package/src/translations/typescript.ts +2 -0
- package/src/validation.ts +2 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, expect } from "vitest";
|
|
2
|
+
import { annotate, shapes } from "./shape";
|
|
3
|
+
|
|
4
|
+
describe("annotate.coerce", (it) => {
|
|
5
|
+
// ── primitives ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
it("coerces str: passthrough for strings", () => {
|
|
8
|
+
expect(annotate.coerce("hello", shapes.str())).toBe("hello");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("coerces str: converts number to string", () => {
|
|
12
|
+
expect(annotate.coerce(42, shapes.str())).toBe("42");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("coerces str: converts boolean to string", () => {
|
|
16
|
+
expect(annotate.coerce(true, shapes.str())).toBe("true");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("coerces str: converts null to string", () => {
|
|
20
|
+
expect(annotate.coerce(null, shapes.str())).toBe("null");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("coerces number: passthrough for numbers", () => {
|
|
24
|
+
expect(annotate.coerce(3.14, shapes.number())).toBe(3.14);
|
|
25
|
+
expect(annotate.coerce(0, shapes.number())).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("coerces number: converts numeric string", () => {
|
|
29
|
+
expect(annotate.coerce("3.14", shapes.number())).toBe(3.14);
|
|
30
|
+
expect(annotate.coerce("", shapes.number())).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("coerces number: returns 0 for non-numeric input", () => {
|
|
34
|
+
expect(annotate.coerce(null, shapes.number())).toBe(0);
|
|
35
|
+
expect(annotate.coerce(false, shapes.number())).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("coerces float: same rules as number", () => {
|
|
39
|
+
expect(annotate.coerce(1.5, shapes.float())).toBe(1.5);
|
|
40
|
+
expect(annotate.coerce("2.5", shapes.float())).toBe(2.5);
|
|
41
|
+
expect(annotate.coerce(null, shapes.float())).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("coerces int: truncates floats", () => {
|
|
45
|
+
expect(annotate.coerce(3.9, shapes.int())).toBe(3);
|
|
46
|
+
expect(annotate.coerce(-3.9, shapes.int())).toBe(-3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("coerces int: converts numeric string", () => {
|
|
50
|
+
expect(annotate.coerce("7.8", shapes.int())).toBe(7);
|
|
51
|
+
expect(annotate.coerce("", shapes.int())).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("coerces int: returns 0 for non-numeric input", () => {
|
|
55
|
+
expect(annotate.coerce(null, shapes.int())).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("coerces bool: passthrough for booleans", () => {
|
|
59
|
+
expect(annotate.coerce(true, shapes.bool())).toBe(true);
|
|
60
|
+
expect(annotate.coerce(false, shapes.bool())).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("coerces bool: truthy string values", () => {
|
|
64
|
+
expect(annotate.coerce("true", shapes.bool())).toBe(true);
|
|
65
|
+
expect(annotate.coerce("1", shapes.bool())).toBe(true);
|
|
66
|
+
expect(annotate.coerce("yes", shapes.bool())).toBe(true);
|
|
67
|
+
expect(annotate.coerce("TRUE", shapes.bool())).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("coerces bool: falsy string values", () => {
|
|
71
|
+
expect(annotate.coerce("false", shapes.bool())).toBe(false);
|
|
72
|
+
expect(annotate.coerce("no", shapes.bool())).toBe(false);
|
|
73
|
+
expect(annotate.coerce("0", shapes.bool())).toBe(false);
|
|
74
|
+
expect(annotate.coerce("", shapes.bool())).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("coerces bool: truthy/falsy non-string values", () => {
|
|
78
|
+
expect(annotate.coerce(1, shapes.bool())).toBe(true);
|
|
79
|
+
expect(annotate.coerce(0, shapes.bool())).toBe(false);
|
|
80
|
+
expect(annotate.coerce(null, shapes.bool())).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("coerces nil: always returns null", () => {
|
|
84
|
+
expect(annotate.coerce("anything", shapes.nil())).toBeNull();
|
|
85
|
+
expect(annotate.coerce(42, shapes.nil())).toBeNull();
|
|
86
|
+
expect(annotate.coerce(null, shapes.nil())).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("coerces unknown: returns value as-is", () => {
|
|
90
|
+
const obj = { x: 1 };
|
|
91
|
+
expect(annotate.coerce(obj, shapes.unknown())).toBe(obj);
|
|
92
|
+
expect(annotate.coerce(null, shapes.unknown())).toBeNull();
|
|
93
|
+
expect(annotate.coerce(42, shapes.unknown())).toBe(42);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("coerces date: passthrough for Date instances", () => {
|
|
97
|
+
const d = new Date("2024-01-01");
|
|
98
|
+
expect(annotate.coerce(d, shapes.date())).toBe(d);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("coerces date: parses ISO string", () => {
|
|
102
|
+
const result = annotate.coerce("2024-06-15", shapes.date());
|
|
103
|
+
expect(result).toBeInstanceOf(Date);
|
|
104
|
+
expect((result as Date).getFullYear()).toBe(2024);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("coerces date: parses numeric timestamp", () => {
|
|
108
|
+
const ts = 1700000000000;
|
|
109
|
+
const result = annotate.coerce(ts, shapes.date());
|
|
110
|
+
expect(result).toBeInstanceOf(Date);
|
|
111
|
+
expect((result as Date).getTime()).toBe(ts);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("coerces date: returns a Date for unknown input", () => {
|
|
115
|
+
expect(annotate.coerce(null, shapes.date())).toBeInstanceOf(Date);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── literals ─────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
it("coerces literalStr: always returns the literal value", () => {
|
|
121
|
+
expect(annotate.coerce("anything", shapes.literalStr("hello"))).toBe(
|
|
122
|
+
"hello",
|
|
123
|
+
);
|
|
124
|
+
expect(annotate.coerce(42, shapes.literalStr("hello"))).toBe("hello");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("coerces literalBool: always returns the literal value", () => {
|
|
128
|
+
expect(annotate.coerce("false", shapes.literalBool(true))).toBe(true);
|
|
129
|
+
expect(annotate.coerce(99, shapes.literalBool(false))).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("coerces literalInt / literalFloat / literalNumber: always returns the literal value", () => {
|
|
133
|
+
expect(annotate.coerce("anything", shapes.literalInt(5))).toBe(5);
|
|
134
|
+
expect(annotate.coerce("anything", shapes.literalFloat(1.5))).toBe(1.5);
|
|
135
|
+
expect(annotate.coerce("anything", shapes.literalNumber(42))).toBe(42);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── bytes ────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
it("coerces bytes: passthrough for Uint8Array", () => {
|
|
141
|
+
const buf = new Uint8Array([1, 2, 3]);
|
|
142
|
+
expect(annotate.coerce(buf, shapes.bytes())).toBe(buf);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("coerces bytes: converts number array", () => {
|
|
146
|
+
expect(annotate.coerce([10, 20, 30], shapes.bytes())).toEqual(
|
|
147
|
+
new Uint8Array([10, 20, 30]),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("coerces bytes: parses comma-separated string", () => {
|
|
152
|
+
expect(annotate.coerce("1,2,3", shapes.bytes())).toEqual(
|
|
153
|
+
new Uint8Array([1, 2, 3]),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("coerces bytes: returns empty Uint8Array for unknown input", () => {
|
|
158
|
+
expect(annotate.coerce(null, shapes.bytes())).toEqual(new Uint8Array());
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── array ────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
it("coerces array: passthrough for arrays", () => {
|
|
164
|
+
expect(annotate.coerce([1, 2, 3], shapes.array(shapes.int()))).toEqual([
|
|
165
|
+
1, 2, 3,
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("coerces array: converts each item", () => {
|
|
170
|
+
expect(
|
|
171
|
+
annotate.coerce(["1", "2", "3"], shapes.array(shapes.int())),
|
|
172
|
+
).toEqual([1, 2, 3]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("coerces array: parses comma-separated string", () => {
|
|
176
|
+
expect(annotate.coerce("10, 20, 30", shapes.array(shapes.int()))).toEqual([
|
|
177
|
+
10, 20, 30,
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("coerces array: returns empty array for other types", () => {
|
|
182
|
+
expect(annotate.coerce(null, shapes.array(shapes.str()))).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── tuple ────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
it("coerces tuple: converts array elements per shape", () => {
|
|
188
|
+
expect(
|
|
189
|
+
annotate.coerce(["42", true], shapes.tuple(shapes.int(), shapes.bool())),
|
|
190
|
+
).toEqual([42, true]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("coerces tuple: parses comma-separated string", () => {
|
|
194
|
+
expect(
|
|
195
|
+
annotate.coerce("hello,1", shapes.tuple(shapes.str(), shapes.int())),
|
|
196
|
+
).toEqual(["hello", 1]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("coerces tuple: fills missing elements with coerced nulls", () => {
|
|
200
|
+
const result = annotate.coerce(
|
|
201
|
+
null,
|
|
202
|
+
shapes.tuple(shapes.int(), shapes.str()),
|
|
203
|
+
);
|
|
204
|
+
expect(result).toEqual([0, "null"]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ── vector ───────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
it("coerces vector: passthrough for number array", () => {
|
|
210
|
+
expect(annotate.coerce([1, 2, 3], shapes.vector(3))).toEqual([1, 2, 3]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("coerces vector: parses comma-separated string", () => {
|
|
214
|
+
expect(annotate.coerce("1.0, 2.0, 3.0", shapes.vector(3))).toEqual([
|
|
215
|
+
1, 2, 3,
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("coerces vector: returns empty array for unknown input", () => {
|
|
220
|
+
expect(annotate.coerce(null, shapes.vector(3))).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── mapping ──────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
it("coerces mapping: passthrough for plain objects", () => {
|
|
226
|
+
expect(
|
|
227
|
+
annotate.coerce(
|
|
228
|
+
{ name: "Alice", age: 30 },
|
|
229
|
+
shapes.mapping({ name: shapes.str(), age: shapes.int() }),
|
|
230
|
+
),
|
|
231
|
+
).toEqual({ name: "Alice", age: 30 });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("coerces mapping: coerces each field's type", () => {
|
|
235
|
+
expect(
|
|
236
|
+
annotate.coerce(
|
|
237
|
+
{ name: 42, age: "30" },
|
|
238
|
+
shapes.mapping({ name: shapes.str(), age: shapes.int() }),
|
|
239
|
+
),
|
|
240
|
+
).toEqual({ name: "42", age: 30 });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("coerces mapping: parses JSON string", () => {
|
|
244
|
+
expect(
|
|
245
|
+
annotate.coerce(
|
|
246
|
+
'{"x": "1", "y": "2"}',
|
|
247
|
+
shapes.mapping({ x: shapes.int(), y: shapes.int() }),
|
|
248
|
+
),
|
|
249
|
+
).toEqual({ x: 1, y: 2 });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("coerces mapping: fills missing fields with defaults", () => {
|
|
253
|
+
expect(
|
|
254
|
+
annotate.coerce(
|
|
255
|
+
{},
|
|
256
|
+
shapes.mapping({ count: shapes.int(), label: shapes.str() }),
|
|
257
|
+
),
|
|
258
|
+
).toEqual({ count: 0, label: "undefined" });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("coerces mapping: returns coerced defaults for non-object input", () => {
|
|
262
|
+
expect(
|
|
263
|
+
annotate.coerce(null, shapes.mapping({ active: shapes.bool() })),
|
|
264
|
+
).toEqual({ active: false });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ── ref ───────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
it("coerces ref: returns value as-is", () => {
|
|
270
|
+
const val = { id: 1 };
|
|
271
|
+
expect(annotate.coerce(val, shapes.ref("MyType"))).toBe(val);
|
|
272
|
+
expect(annotate.coerce("anything", shapes.ref("MyType"))).toBe("anything");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── union ─────────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
it("coerces union: uses first member's shape", () => {
|
|
278
|
+
const shape = shapes.union(shapes.int(), shapes.str());
|
|
279
|
+
expect(annotate.coerce("42", shape)).toBe(42);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("coerces union: handles single-member union", () => {
|
|
283
|
+
expect(annotate.coerce("hello", shapes.union(shapes.str()))).toBe("hello");
|
|
284
|
+
});
|
|
285
|
+
});
|
package/src/shape.ts
CHANGED
|
@@ -107,6 +107,7 @@ export interface ShapeStr extends ShapeWithOutput<"str", string> {}
|
|
|
107
107
|
export interface ShapeDate extends ShapeWithOutput<"date", Date> {}
|
|
108
108
|
export interface ShapeNil extends ShapeWithOutput<"nil", null> {}
|
|
109
109
|
export interface ShapeUnknown extends ShapeWithOutput<"unknown", unknown> {}
|
|
110
|
+
export interface ShapeBytes extends ShapeWithOutput<"bytes", Uint8Array> {}
|
|
110
111
|
|
|
111
112
|
export interface ShapeLiteralFloat<
|
|
112
113
|
Value extends number = number,
|
|
@@ -197,6 +198,7 @@ export type Shape =
|
|
|
197
198
|
| ShapeDate
|
|
198
199
|
| ShapeNil
|
|
199
200
|
| ShapeUnknown
|
|
201
|
+
| ShapeBytes
|
|
200
202
|
| ShapeLiteralFloat
|
|
201
203
|
| ShapeLiteralInt
|
|
202
204
|
| ShapeLiteralNumber
|
|
@@ -288,6 +290,14 @@ export namespace shapes {
|
|
|
288
290
|
};
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
export function bytes(): ShapeBytes {
|
|
294
|
+
return {
|
|
295
|
+
type: "bytes",
|
|
296
|
+
anno: {},
|
|
297
|
+
...createPhantom(),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
291
301
|
export function literalFloat<Value extends number>(
|
|
292
302
|
value: Value,
|
|
293
303
|
): ShapeLiteralFloat<Value> {
|
|
@@ -539,4 +549,117 @@ export namespace annotate {
|
|
|
539
549
|
}
|
|
540
550
|
return shapes.unknown();
|
|
541
551
|
}
|
|
552
|
+
|
|
553
|
+
export function coerce<S extends Shape>(
|
|
554
|
+
x: unknown,
|
|
555
|
+
shape: S,
|
|
556
|
+
): OutputOfShape<S> {
|
|
557
|
+
const convert = (x: unknown, shape: Shape): unknown => {
|
|
558
|
+
switch (shape.type) {
|
|
559
|
+
case "str":
|
|
560
|
+
if (typeof x === "string") return x;
|
|
561
|
+
return `${x}`;
|
|
562
|
+
case "literal-str":
|
|
563
|
+
case "literal-int":
|
|
564
|
+
case "literal-float":
|
|
565
|
+
case "literal-number":
|
|
566
|
+
case "literal-bool":
|
|
567
|
+
return shape.value;
|
|
568
|
+
case "number":
|
|
569
|
+
case "float":
|
|
570
|
+
if (typeof x === "number") return x;
|
|
571
|
+
if (typeof x === "string") return Number(x || 0);
|
|
572
|
+
return 0;
|
|
573
|
+
case "int":
|
|
574
|
+
if (typeof x === "number") return Math.trunc(x);
|
|
575
|
+
if (typeof x === "string") return Math.trunc(Number(x || 0));
|
|
576
|
+
return 0;
|
|
577
|
+
case "nil":
|
|
578
|
+
return null;
|
|
579
|
+
case "bool":
|
|
580
|
+
if (typeof x === "boolean") return x;
|
|
581
|
+
if (typeof x === "string")
|
|
582
|
+
return ["true", "1", "yes"].includes(x.trim().toLowerCase());
|
|
583
|
+
return !!x;
|
|
584
|
+
case "date":
|
|
585
|
+
if (x instanceof Date) return x;
|
|
586
|
+
if (typeof x === "string" || typeof x === "number")
|
|
587
|
+
return new Date(x);
|
|
588
|
+
return new Date();
|
|
589
|
+
case "unknown":
|
|
590
|
+
return x;
|
|
591
|
+
case "vector":
|
|
592
|
+
if (Array.isArray(x)) return x.map((v) => convert(v, shapes.float()));
|
|
593
|
+
if (typeof x === "string")
|
|
594
|
+
return x
|
|
595
|
+
.replaceAll(/\[|\{|\}|\]/g, "")
|
|
596
|
+
.split(",")
|
|
597
|
+
.map((v) => v.trim())
|
|
598
|
+
.map((v) => convert(v, shapes.float()));
|
|
599
|
+
return [];
|
|
600
|
+
case "array":
|
|
601
|
+
if (Array.isArray(x)) return x.map((v) => convert(v, shape.item));
|
|
602
|
+
if (typeof x === "string")
|
|
603
|
+
return x
|
|
604
|
+
.replaceAll(/\[|\{|\}|\]/g, "")
|
|
605
|
+
.split(",")
|
|
606
|
+
.map((v) => v.trim())
|
|
607
|
+
.map((v) => convert(v, shape.item));
|
|
608
|
+
return [];
|
|
609
|
+
case "tuple":
|
|
610
|
+
if (Array.isArray(x))
|
|
611
|
+
return shape.tup.map((s, i) => convert(x[i], s));
|
|
612
|
+
if (typeof x === "string") {
|
|
613
|
+
const parts = x
|
|
614
|
+
.replaceAll(/\[|\{|\}|\]/g, "")
|
|
615
|
+
.split(",")
|
|
616
|
+
.map((v) => v.trim());
|
|
617
|
+
return shape.tup.map((s, i) => convert(parts[i], s));
|
|
618
|
+
}
|
|
619
|
+
return shape.tup.map((s) => convert(null, s));
|
|
620
|
+
case "bytes":
|
|
621
|
+
if (x instanceof Uint8Array) return x;
|
|
622
|
+
if (Array.isArray(x))
|
|
623
|
+
return new Uint8Array(
|
|
624
|
+
x.map((v) => convert(v, shapes.int())) as number[],
|
|
625
|
+
);
|
|
626
|
+
if (typeof x === "string")
|
|
627
|
+
return new Uint8Array(
|
|
628
|
+
x
|
|
629
|
+
.replaceAll(/\[|\{|\}|\]/g, "")
|
|
630
|
+
.split(",")
|
|
631
|
+
.map((v) => v.trim())
|
|
632
|
+
.map((v) => convert(v, shapes.number())) as number[],
|
|
633
|
+
);
|
|
634
|
+
return new Uint8Array();
|
|
635
|
+
case "ref":
|
|
636
|
+
return x;
|
|
637
|
+
case "union":
|
|
638
|
+
const t = shape.ofs[0];
|
|
639
|
+
return convert(x, t || shapes.unknown());
|
|
640
|
+
case "mapping":
|
|
641
|
+
let obj: Record<string, unknown> = {};
|
|
642
|
+
if (isPlainObject(x)) {
|
|
643
|
+
obj = x;
|
|
644
|
+
} else if (typeof x === "string") {
|
|
645
|
+
const trimmed = x.trim();
|
|
646
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
647
|
+
try {
|
|
648
|
+
obj = JSON.parse(x) as Record<string, unknown>;
|
|
649
|
+
} catch {}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return Object.assign(
|
|
653
|
+
{},
|
|
654
|
+
...Object.entries(shape.rec).map(([k, t]) => {
|
|
655
|
+
return {
|
|
656
|
+
[k]: convert(obj[k], t),
|
|
657
|
+
};
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
return convert(x, shape) as OutputOfShape<S>;
|
|
664
|
+
}
|
|
542
665
|
}
|
|
@@ -12,6 +12,7 @@ describe("typescript", (it) => {
|
|
|
12
12
|
expect(typescript(shapes.date())).toBe("Date");
|
|
13
13
|
expect(typescript(shapes.nil())).toBe("null");
|
|
14
14
|
expect(typescript(shapes.unknown())).toBe("unknown");
|
|
15
|
+
expect(typescript(shapes.bytes())).toBe("Uint8Array");
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
it("translates unions", () => {
|
package/src/validation.ts
CHANGED
|
@@ -94,6 +94,8 @@ export function validate(x: unknown, shape: Shape): ValidationResult {
|
|
|
94
94
|
return maybe(x instanceof Date, "expected a date", path);
|
|
95
95
|
case "unknown":
|
|
96
96
|
return ok();
|
|
97
|
+
case "bytes":
|
|
98
|
+
return maybe(x instanceof Uint8Array, "expected a Uint8Array", path);
|
|
97
99
|
case "array": {
|
|
98
100
|
if (!Array.isArray(x)) return err("expected an array", path);
|
|
99
101
|
if (shape.anno.min !== undefined && x.length < shape.anno.min)
|