typespec-http-zod 0.26.0 → 0.28.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/README.md +6 -6
- package/dist/src/api.d.ts +63 -9
- package/dist/src/api.js +710 -295
- package/dist/src/constraints.d.ts +46 -0
- package/dist/src/constraints.js +237 -10
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/lib.d.ts +32 -3
- package/dist/src/lib.js +53 -0
- package/dist/src/linter.d.ts +15 -0
- package/dist/src/linter.js +23 -0
- package/dist/src/registry.d.ts +1 -1
- package/dist/src/registry.js +161 -41
- package/dist/src/results.d.ts +113 -0
- package/dist/src/results.js +134 -0
- package/dist/src/rules/redos-prone-pattern.rule.d.ts +27 -0
- package/dist/src/rules/redos-prone-pattern.rule.js +47 -0
- package/dist/src/statuses.d.ts +24 -0
- package/dist/src/statuses.js +27 -0
- package/dist/src/types.js +37 -24
- package/dist/src/uri-template.d.ts +1 -1
- package/dist/src/uri-template.js +4 -1
- package/dist/src/zod.d.ts +25 -18
- package/dist/src/zod.js +69 -14
- package/package.json +19 -16
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The status codes a server can answer with, by class.** A `4XX` range or a `default` response
|
|
3
|
+
* means "every code in these classes that nothing more specific claims", and a type for such a
|
|
4
|
+
* result needs those codes spelled out: `number` would admit `999`, and a type that admits a status
|
|
5
|
+
* no server can send is a type no client can narrow on.
|
|
6
|
+
*
|
|
7
|
+
* **One list, owned here, because two server emitters each held a copy.** `typespec-hono` and
|
|
8
|
+
* `typespec-http-mcp` both typed a ranged or `default` result from their own table, and the contract
|
|
9
|
+
* types in `requests.gen.ts` need the same one to state a result a backend can hand straight to
|
|
10
|
+
* either. Three copies of a table that must agree are three tables that will not.
|
|
11
|
+
*
|
|
12
|
+
* **Graded outside this package, against Hono's own `http-status.d.ts`,** by the two emitters that
|
|
13
|
+
* serve through it - an expectation that does not come from the code it grades. A code Hono adds
|
|
14
|
+
* turns their arm red here first.
|
|
15
|
+
*/
|
|
16
|
+
export const STATUS_CODES = {
|
|
17
|
+
1: [100, 101, 102, 103],
|
|
18
|
+
2: [200, 201, 202, 203, 204, 205, 206, 207, 208, 226],
|
|
19
|
+
3: [300, 301, 302, 303, 304, 305, 306, 307, 308],
|
|
20
|
+
4: [
|
|
21
|
+
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
|
|
22
|
+
421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
|
|
23
|
+
],
|
|
24
|
+
5: [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511],
|
|
25
|
+
};
|
|
26
|
+
/** The codes a response cannot carry a body under, per RFC 9110: 101, 204, 205 and 304. */
|
|
27
|
+
export const CONTENTLESS_STATUS_CODES = [101, 204, 205, 304];
|
package/dist/src/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getDiscriminatedUnion,
|
|
1
|
+
import { getDiscriminatedUnion, isNeverType, resolveEncodedName, } from "@typespec/compiler";
|
|
2
2
|
import { reportDiagnostic } from "./lib.js";
|
|
3
|
-
import { discriminatedSubtypes, effectiveIndexer, inheritedAndOwnProperties, isOptionalAt, isPayloadProperty, objectKey, refusalTarget, withDeclarationSite, } from "./zod.js";
|
|
3
|
+
import { discriminatedSubtypes, effectiveIndexer, emitsDefault, encodedTypeOf, inheritedAndOwnProperties, isOptionalAt, isPayloadProperty, objectKey, refusalTarget, withDeclarationSite, } from "./zod.js";
|
|
4
4
|
/**
|
|
5
5
|
* TypeSpec type -> plain TypeScript type text.
|
|
6
6
|
*
|
|
@@ -12,9 +12,10 @@ import { discriminatedSubtypes, effectiveIndexer, inheritedAndOwnProperties, isO
|
|
|
12
12
|
* importing the other.
|
|
13
13
|
*
|
|
14
14
|
* **It must agree with `zod.ts` exactly**, because the emitted assertion compares them. Every rule
|
|
15
|
-
* here has a counterpart there: `utcDateTime` is a string on both sides, `?`
|
|
16
|
-
*
|
|
17
|
-
* out of `required` is optional here whether its
|
|
15
|
+
* here has a counterpart there: `utcDateTime` is a string on both sides, `?` is `?: T` because
|
|
16
|
+
* `.exactOptional()` refuses an explicit `undefined` and `?: T | undefined` only where `.default()`
|
|
17
|
+
* accepts one, and a property the document leaves out of `required` is optional here whether its
|
|
18
|
+
* absence is spelled `?` or covered by a default.
|
|
18
19
|
*
|
|
19
20
|
* **The assertion pairs this walk against `z.input`, the type the validator ACCEPTS**, and not
|
|
20
21
|
* against `z.infer`, which is what it produces. These types are the floor a producer supplies, and
|
|
@@ -72,7 +73,7 @@ function refuse(program, type, why, remedy = "") {
|
|
|
72
73
|
}
|
|
73
74
|
function scalarToTs(program, scalar) {
|
|
74
75
|
// A scalar declaration can carry the encoding itself - see `scalarToZod`.
|
|
75
|
-
const encoded =
|
|
76
|
+
const encoded = encodedTypeOf(program, scalar);
|
|
76
77
|
if (encoded !== undefined && encoded !== scalar)
|
|
77
78
|
return scalarToTs(program, encoded);
|
|
78
79
|
let current = scalar;
|
|
@@ -261,9 +262,14 @@ function modelToTs(program, model) {
|
|
|
261
262
|
* annotation claims is by construction the type this walk renders for the same property.
|
|
262
263
|
*/
|
|
263
264
|
export function propertyTypeToTs(program, property) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
/**
|
|
266
|
+
* **`@encode` changes the wire TYPE, and `encodedTypeOf` is the one rule for when it does.** This
|
|
267
|
+
* read `getEncode(...).type` directly, so it missed the exception the Zod walk makes: an array
|
|
268
|
+
* encoding on a QUERY parameter is a style of flattening, not a type, and the validator splits it
|
|
269
|
+
* back into the declared list. So `WireInputs` said `colors: string` for a pipe-delimited query
|
|
270
|
+
* whose handler is given `string[]` - two walks of one property, one of them knowing a rule.
|
|
271
|
+
*/
|
|
272
|
+
const encoded = encodedTypeOf(program, property);
|
|
267
273
|
return typeToTs(program, encoded ?? property.type);
|
|
268
274
|
}
|
|
269
275
|
export function propertyToTs(program, property, wireName) {
|
|
@@ -297,21 +303,28 @@ export function propertyToTs(program, property, wireName) {
|
|
|
297
303
|
* `.default(...)` for it - so the two walks agree without either one restating the rule.
|
|
298
304
|
*/
|
|
299
305
|
/**
|
|
300
|
-
* **`?: T
|
|
306
|
+
* **`?: T`, and `?: T | undefined` only where the validator emits `.default(...)`. The contract
|
|
307
|
+
* type is the validator's INPUT, `z.input`, at every property.**
|
|
308
|
+
*
|
|
309
|
+
* `.exactOptional()` refuses `{ note: undefined }` and `.default(v)` accepts it, so those are the
|
|
310
|
+
* two answers; {@link emitsDefault} says which one this property got. `wire-contract.gen.ts`
|
|
311
|
+
* compares the two walks with nothing widened, so a disagreement here does not compile.
|
|
301
312
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* breaks the most ordinary handler there is.** `createWidget: (ctx, input) => ok(input)` - return
|
|
305
|
-
* what you were given - stopped compiling, because what a handler RECEIVES carries
|
|
306
|
-
* `?: T | undefined` and what it would then have to SUPPLY did not.
|
|
313
|
+
* **This was `?: T | undefined` for every optional, kept deliberately on three reasons that were
|
|
314
|
+
* each measured false** once `0.24.0` moved the validator to `.exactOptional()`:
|
|
307
315
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
316
|
+
* - the echo handler `(ctx, input) => ok(input)` broke without it, because what a handler
|
|
317
|
+
* received carried `| undefined`. What it receives is `z.infer` of `.exactOptional()` now,
|
|
318
|
+
* `?: T`, and the echo compiles;
|
|
319
|
+
* - `{ note: undefined }` serialises identically to omitting the key. A server checks a body
|
|
320
|
+
* before it serialises it: a value typed by `WireOutputs` reached `servedBody` and answered
|
|
321
|
+
* 500 for a body whose bytes were valid, and across a service binding it is never serialised;
|
|
322
|
+
* - refusing it costs a producer a conditional spread. That spread infers `{ note?: string }` on
|
|
323
|
+
* TypeScript 5.9 and 7.0 and assigns to the exact type, and it is what a producer under
|
|
324
|
+
* `exactOptionalPropertyTypes` writes for the handler's own return type anyway.
|
|
312
325
|
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
326
|
+
* Two consumers reported the disagreement independently - one carried a `Loosen<T>` to compare the
|
|
327
|
+
* two surfaces - and openapi-typescript, `hc` and `z.infer` all spell an optional `?: T`.
|
|
315
328
|
*/
|
|
316
329
|
/**
|
|
317
330
|
* **Optional AT THIS POSITION, not merely optional in the declaration.** A PATCH body's properties
|
|
@@ -319,9 +332,9 @@ export function propertyToTs(program, property, wireName) {
|
|
|
319
332
|
* what `propertyToZod` has always consulted. This walk read `property.optional` alone, so an
|
|
320
333
|
* update contract demanded every field while the validator beside it accepted any subset.
|
|
321
334
|
*/
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
335
|
+
if (!(property.optional || isOptionalAt(program, property)))
|
|
336
|
+
return `${key}: ${value};`;
|
|
337
|
+
return emitsDefault(program, property) ? `${key}?: ${value} | undefined;` : `${key}?: ${value};`;
|
|
325
338
|
}
|
|
326
339
|
/** Installed by the registry so a named model is declared once and referenced everywhere else. */
|
|
327
340
|
let resolveRef = () => undefined;
|
|
@@ -103,4 +103,4 @@ export declare function explodedQueryCall(spec: {
|
|
|
103
103
|
* gathers those keys back under the parameter's name before the query object is validated: a model
|
|
104
104
|
* takes exactly its declared properties, a record every key no other query parameter claims.
|
|
105
105
|
*/
|
|
106
|
-
export declare const EXPLODED_QUERY_SOURCE = "\n/** RFC 6570 form expansion of an exploded record or model, gathered back under its parameter. */\nfunction explodedQuery(\n\traw: unknown,\n\tspec: {\n\t\treadonly name: string;\n\t\treadonly others: readonly string[];\n\t\treadonly keys: readonly string[] | null;\n\t\treadonly numbers: readonly string[];\n\t\treadonly booleans: readonly string[];\n\t\treadonly values: \"number\" | \"boolean\" | \"\";\n\t},\n): unknown {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)
|
|
106
|
+
export declare const EXPLODED_QUERY_SOURCE = "\n/** RFC 6570 form expansion of an exploded record or model, gathered back under its parameter. */\nfunction explodedQuery(\n\traw: unknown,\n\tspec: {\n\t\treadonly name: string;\n\t\treadonly others: readonly string[];\n\t\treadonly keys: readonly string[] | null;\n\t\treadonly numbers: readonly string[];\n\t\treadonly booleans: readonly string[];\n\t\treadonly values: \"number\" | \"boolean\" | \"\";\n\t},\n): unknown {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) return raw;\n\tconst gathered: Record<string, unknown> = {};\n\tconst rest: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(raw)) {\n\t\t// Already gathered only where an OBJECT sits under the name: a query value from the wire is a\n\t\t// string or a list of them, and `{ param: 0 }` expands to `?param=0`.\n\t\tif (key === spec.name && value !== null && typeof value === \"object\" && !Array.isArray(value)) return raw;\n\t\tconst own = spec.keys === null ? !spec.others.includes(key) : spec.keys.includes(key);\n\t\tif (!own) {\n\t\t\trest[key] = value;\n\t\t\tcontinue;\n\t\t}\n\t\tconst kind = spec.values !== \"\" ? spec.values : spec.numbers.includes(key) ? \"number\" : spec.booleans.includes(key) ? \"boolean\" : \"\";\n\t\tgathered[key] =\n\t\t\ttypeof value !== \"string\"\n\t\t\t\t? value\n\t\t\t\t: kind === \"number\" && value.trim() !== \"\" && Number.isFinite(Number(value))\n\t\t\t\t\t? Number(value)\n\t\t\t\t\t: kind === \"boolean\" && (value === \"true\" || value === \"false\")\n\t\t\t\t\t\t? value === \"true\"\n\t\t\t\t\t\t: value;\n\t}\n\treturn Object.keys(gathered).length === 0 ? raw : { ...rest, [spec.name]: gathered };\n}\n";
|
package/dist/src/uri-template.js
CHANGED
|
@@ -230,10 +230,13 @@ function explodedQuery(
|
|
|
230
230
|
readonly values: "number" | "boolean" | "";
|
|
231
231
|
},
|
|
232
232
|
): unknown {
|
|
233
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)
|
|
233
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
234
234
|
const gathered: Record<string, unknown> = {};
|
|
235
235
|
const rest: Record<string, unknown> = {};
|
|
236
236
|
for (const [key, value] of Object.entries(raw)) {
|
|
237
|
+
// Already gathered only where an OBJECT sits under the name: a query value from the wire is a
|
|
238
|
+
// string or a list of them, and \`{ param: 0 }\` expands to \`?param=0\`.
|
|
239
|
+
if (key === spec.name && value !== null && typeof value === "object" && !Array.isArray(value)) return raw;
|
|
237
240
|
const own = spec.keys === null ? !spec.others.includes(key) : spec.keys.includes(key);
|
|
238
241
|
if (!own) {
|
|
239
242
|
rest[key] = value;
|
package/dist/src/zod.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import { type Enum, type Model, type ModelProperty, type Program, type Type } from "@typespec/compiler";
|
|
1
|
+
import { type Enum, type Model, type ModelProperty, type Program, type Scalar, type Type } from "@typespec/compiler";
|
|
2
2
|
import { Visibility } from "@typespec/http";
|
|
3
|
+
/**
|
|
4
|
+
* What a walk returns after reporting a refusal.
|
|
5
|
+
*
|
|
6
|
+
* **Reporting does not unwind, which is the point.** A throw abandoned the compile at the first
|
|
7
|
+
* unrepresentable construct; reporting lets the walk carry on and name every one of them in a single
|
|
8
|
+
* run. The walk still has to return a `string`, and `z.never()` is the honest placeholder: valid
|
|
9
|
+
* Zod, rejects everything, and unreachable in practice because the error diagnostic has already
|
|
10
|
+
* failed the compile that would have written it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const UNREPRESENTABLE = "z.never()";
|
|
3
13
|
export declare function withDeferredAnnotator<T>(annotator: (property: ModelProperty) => string | undefined, run: () => T): T;
|
|
4
14
|
export declare function withSealedObjects<T>(sealed: boolean, run: () => T): T;
|
|
5
15
|
export declare function withVisibility<T>(program: Program, next: Visibility, run: () => T): T;
|
|
@@ -98,23 +108,6 @@ export declare function effectiveIndexer(model: Model): Model["indexer"];
|
|
|
98
108
|
* for a real subtype - the same rule that decides whether a base may be sealed.
|
|
99
109
|
*/
|
|
100
110
|
export declare function discriminatedSubtypes(program: Program, model: Model): readonly Model[];
|
|
101
|
-
/**
|
|
102
|
-
* **What JSON kind a value of this type arrives as, for a parameter carried in text.**
|
|
103
|
-
*
|
|
104
|
-
* A path, query or header value is always a string on the wire, so a numeric or boolean parameter has
|
|
105
|
-
* to be decoded before the document's schema runs. Deciding WHICH decoder applies used to be done by
|
|
106
|
-
* inspecting the emitted Zod - `declared.startsWith("z.number()")` - which is a string about a string
|
|
107
|
-
* and fails silently the moment the expression is spelled any other way.
|
|
108
|
-
*
|
|
109
|
-
* It did. `@query size: 10 | 25 | 50` emits `z.union([z.literal(10), ...])`, which starts with
|
|
110
|
-
* neither, so no decoder was wrapped around it and the validator required the NUMBER 25 from a wire
|
|
111
|
-
* that can only deliver `"25"`. Measured: `?size=25` rejected, `size: 25` accepted. Every conformant
|
|
112
|
-
* caller got a 400, and no document comparison could see it, because both sides agree the parameter
|
|
113
|
-
* is a number - the disagreement is with the wire.
|
|
114
|
-
*
|
|
115
|
-
* Reading the TYPE answers the question the emitted text was standing in for. `SCALARS` remains the
|
|
116
|
-
* single list of what becomes a number, so this cannot drift from what `scalarToZod` emits.
|
|
117
|
-
*/
|
|
118
111
|
export declare function wireKindOf(program: Program, type: Type): "number" | "boolean" | undefined;
|
|
119
112
|
/**
|
|
120
113
|
* A name as an object key - quoted when it is not a legal JavaScript identifier.
|
|
@@ -150,7 +143,21 @@ export declare function objectKey(name: string): string;
|
|
|
150
143
|
* comment indented differently from the member it documents is the tell that a generator wrote it.
|
|
151
144
|
*/
|
|
152
145
|
export declare function jsDocComment(text: string | undefined, indent: string): string;
|
|
146
|
+
export declare function encodedTypeOf(program: Program, target: ModelProperty | Scalar): Scalar | undefined;
|
|
153
147
|
export declare function propertyToZod(program: Program, property: ModelProperty): string;
|
|
148
|
+
/**
|
|
149
|
+
* **Whether `propertyToZod` emits `.default(...)` for this property, which is exactly whether its
|
|
150
|
+
* INPUT admits an explicit `undefined`.**
|
|
151
|
+
*
|
|
152
|
+
* `.exactOptional()` refuses `{ p: undefined }`; `.default(v)` accepts it and fills `v`. The contract
|
|
153
|
+
* types in `requests.gen.ts` state the input, so they have to know which of the two this emitted -
|
|
154
|
+
* and asking this function rather than restating its rule is what keeps the two walks agreeing.
|
|
155
|
+
* A default on a REQUIRED property emits neither (`default-on-required-property`), and one whose
|
|
156
|
+
* value cannot be rendered emits no `.default()` (`unsupported-default`); both fall out here.
|
|
157
|
+
*
|
|
158
|
+
* Pure: it reports nothing, so the type walk can ask it without duplicating a diagnostic.
|
|
159
|
+
*/
|
|
160
|
+
export declare function emitsDefault(program: Program, property: ModelProperty): boolean;
|
|
154
161
|
export declare function withRefResolver<T>(resolver: (type: Type) => string | undefined, run: () => T): T;
|
|
155
162
|
/** Called by the registry when it hands out the identifier of a declaration that is still open. */
|
|
156
163
|
export declare function noteBackEdge(): void;
|
package/dist/src/zod.js
CHANGED
|
@@ -11,7 +11,7 @@ import { reportDiagnostic } from "./lib.js";
|
|
|
11
11
|
* Zod, rejects everything, and unreachable in practice because the error diagnostic has already
|
|
12
12
|
* failed the compile that would have written it.
|
|
13
13
|
*/
|
|
14
|
-
const UNREPRESENTABLE = "z.never()";
|
|
14
|
+
export const UNREPRESENTABLE = "z.never()";
|
|
15
15
|
/**
|
|
16
16
|
* **Installed by the registry, which is the only place that knows what a declared type is CALLED.**
|
|
17
17
|
*
|
|
@@ -339,16 +339,36 @@ const SCALARS = {
|
|
|
339
339
|
string: "z.string()",
|
|
340
340
|
boolean: "z.boolean()",
|
|
341
341
|
bytes: "z.string()",
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
342
|
+
/**
|
|
343
|
+
* **A declared WIDTH is a claim about the value too, and ten of them used to share one check.**
|
|
344
|
+
*
|
|
345
|
+
* Every integer scalar emitted `z.number().int()`, which is "an integer within the safe range" and
|
|
346
|
+
* nothing more. Measured at the wire on `workerd`, against a document publishing the widths below:
|
|
347
|
+
* a `uint8` accepted `-5` and `100000`, an `int32` accepted `3000000000` and `-3000000000`.
|
|
348
|
+
*
|
|
349
|
+
* The differential oracle cannot see it. JSON Schema 2020-12 treats `format` as an annotation, so
|
|
350
|
+
* Ajv on the published document answers VALID for all of those and agrees with the server while
|
|
351
|
+
* both are wrong. Ajv says so out loud: `unknown format "uint8" ignored`.
|
|
352
|
+
*
|
|
353
|
+
* Zod 4 has the named formats for the two widths that have them, and they emit better JSON Schema
|
|
354
|
+
* than a bare bound would. The rest take explicit bounds.
|
|
355
|
+
*
|
|
356
|
+
* **`int64` and `uint64` keep the safe-integer ceiling, and that is a limit rather than a
|
|
357
|
+
* choice.** `JSON.parse` has already lost a value above 2^53 before any validator runs, so the
|
|
358
|
+
* alternatives are to refuse it or to accept a number that is not the one the caller sent.
|
|
359
|
+
* Refusing is the honest one. `z.int64()` exists but is a `bigint`, which a JSON body cannot
|
|
360
|
+
* carry. What changes here is only that `uint64` stops accepting negatives.
|
|
361
|
+
*/
|
|
362
|
+
int8: "z.int().min(-128).max(127)",
|
|
363
|
+
int16: "z.int().min(-32768).max(32767)",
|
|
364
|
+
int32: "z.int32()",
|
|
365
|
+
int64: "z.int()",
|
|
366
|
+
safeint: "z.int()",
|
|
367
|
+
uint8: "z.int().min(0).max(255)",
|
|
368
|
+
uint16: "z.int().min(0).max(65535)",
|
|
369
|
+
uint32: "z.uint32()",
|
|
370
|
+
uint64: "z.int().min(0)",
|
|
371
|
+
integer: "z.int()",
|
|
352
372
|
float: "z.number()",
|
|
353
373
|
float32: "z.number()",
|
|
354
374
|
float64: "z.number()",
|
|
@@ -859,8 +879,25 @@ function modelToZod(program, model) {
|
|
|
859
879
|
* is a number - the disagreement is with the wire.
|
|
860
880
|
*
|
|
861
881
|
* Reading the TYPE answers the question the emitted text was standing in for. `SCALARS` remains the
|
|
862
|
-
* single list of what becomes a number
|
|
882
|
+
* single list of what becomes a number.
|
|
883
|
+
*
|
|
884
|
+
* **It still inspects the emitted expression to decide WHICH kind, and that did drift once more.**
|
|
885
|
+
* The test was `startsWith("z.number()")`, and giving each integer width its declared bounds spelled
|
|
886
|
+
* nine of them `z.int(`, `z.int32(` and `z.uint32(` instead. Every one silently lost its wire
|
|
887
|
+
* decoder: 22 arms went red at once, which is the only reason it was not shipped. The prefixes live
|
|
888
|
+
* in one named constant now.
|
|
889
|
+
*
|
|
890
|
+
* **That claim named a `scalars.test.ts` which did not exist, and nothing graded the list.**
|
|
891
|
+
* Measured: dropping `z.uint32(` from it, and separately `z.int(`, left all 468 arms in this
|
|
892
|
+
* package green, because no fixture had ever declared an integer width as a PARAMETER - the only
|
|
893
|
+
* place the decoder is attached. `test/widths/` now declares all ten as query parameters and sends
|
|
894
|
+
* each as text, so a spelling this list forgets fails here rather than at a consumer.
|
|
863
895
|
*/
|
|
896
|
+
/**
|
|
897
|
+
* Every emitted expression that denotes a JSON number. Named once, because `SCALARS` and this list
|
|
898
|
+
* have to agree and nothing about the two being in the same file makes them.
|
|
899
|
+
*/
|
|
900
|
+
const NUMERIC_EXPRESSIONS = ["z.number()", "z.int(", "z.int32(", "z.uint32("];
|
|
864
901
|
export function wireKindOf(program, type) {
|
|
865
902
|
if (type.kind === "Scalar") {
|
|
866
903
|
const encoded = encodedTypeOf(program, type);
|
|
@@ -870,7 +907,7 @@ export function wireKindOf(program, type) {
|
|
|
870
907
|
const mapped = SCALARS[current.name];
|
|
871
908
|
if (mapped === undefined)
|
|
872
909
|
continue;
|
|
873
|
-
if (mapped.startsWith(
|
|
910
|
+
if (NUMERIC_EXPRESSIONS.some((prefix) => mapped.startsWith(prefix)))
|
|
874
911
|
return "number";
|
|
875
912
|
if (mapped === "z.boolean()")
|
|
876
913
|
return "boolean";
|
|
@@ -1026,7 +1063,7 @@ const PARAMETER_STYLE_ENCODINGS = new Set([
|
|
|
1026
1063
|
"ArrayEncoding.commaDelimited",
|
|
1027
1064
|
"ArrayEncoding.newlineDelimited",
|
|
1028
1065
|
]);
|
|
1029
|
-
function encodedTypeOf(program, target) {
|
|
1066
|
+
export function encodedTypeOf(program, target) {
|
|
1030
1067
|
const encoded = getEncode(program, target);
|
|
1031
1068
|
if (encoded === undefined)
|
|
1032
1069
|
return undefined;
|
|
@@ -1172,6 +1209,24 @@ function renderValue(value) {
|
|
|
1172
1209
|
}
|
|
1173
1210
|
return undefined;
|
|
1174
1211
|
}
|
|
1212
|
+
/**
|
|
1213
|
+
* **Whether `propertyToZod` emits `.default(...)` for this property, which is exactly whether its
|
|
1214
|
+
* INPUT admits an explicit `undefined`.**
|
|
1215
|
+
*
|
|
1216
|
+
* `.exactOptional()` refuses `{ p: undefined }`; `.default(v)` accepts it and fills `v`. The contract
|
|
1217
|
+
* types in `requests.gen.ts` state the input, so they have to know which of the two this emitted -
|
|
1218
|
+
* and asking this function rather than restating its rule is what keeps the two walks agreeing.
|
|
1219
|
+
* A default on a REQUIRED property emits neither (`default-on-required-property`), and one whose
|
|
1220
|
+
* value cannot be rendered emits no `.default()` (`unsupported-default`); both fall out here.
|
|
1221
|
+
*
|
|
1222
|
+
* Pure: it reports nothing, so the type walk can ask it without duplicating a diagnostic.
|
|
1223
|
+
*/
|
|
1224
|
+
export function emitsDefault(program, property) {
|
|
1225
|
+
const optional = property.optional || isOptionalAt(program, property);
|
|
1226
|
+
return (optional &&
|
|
1227
|
+
property.defaultValue !== undefined &&
|
|
1228
|
+
renderValue(property.defaultValue) !== undefined);
|
|
1229
|
+
}
|
|
1175
1230
|
/** A property's declared default, as a JS literal, or `undefined` when it has none. */
|
|
1176
1231
|
function defaultOf(program, property) {
|
|
1177
1232
|
const value = property.defaultValue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "TypeSpec emitter and library: turn an HTTP service definition into Zod validators and framework-free contract types that agree, keyword for keyword, with the OpenAPI document @typespec/openapi3 publishes from the same source.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
@@ -42,19 +42,22 @@
|
|
|
42
42
|
"access": "public",
|
|
43
43
|
"provenance": true
|
|
44
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"redos-detector": "^6.1.4"
|
|
47
|
+
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"@types/node": "^26.5.1",
|
|
47
|
-
"@typespec/compiler": "1.
|
|
48
|
-
"@typespec/events": "0.
|
|
49
|
-
"@typespec/http": "1.
|
|
50
|
-
"@typespec/http-specs": "0.1.0-alpha.
|
|
51
|
-
"@typespec/openapi": "1.
|
|
52
|
-
"@typespec/openapi3": "1.
|
|
53
|
-
"@typespec/rest": "0.
|
|
54
|
-
"@typespec/sse": "0.
|
|
55
|
-
"@typespec/streams": "0.
|
|
56
|
-
"@typespec/versioning": "0.
|
|
57
|
-
"@typespec/xml": "0.
|
|
50
|
+
"@typespec/compiler": "1.17.0",
|
|
51
|
+
"@typespec/events": "0.87.0",
|
|
52
|
+
"@typespec/http": "1.17.0",
|
|
53
|
+
"@typespec/http-specs": "0.1.0-alpha.44",
|
|
54
|
+
"@typespec/openapi": "1.17.0",
|
|
55
|
+
"@typespec/openapi3": "1.17.0",
|
|
56
|
+
"@typespec/rest": "0.87.0",
|
|
57
|
+
"@typespec/sse": "0.87.0",
|
|
58
|
+
"@typespec/streams": "0.87.0",
|
|
59
|
+
"@typespec/versioning": "0.87.0",
|
|
60
|
+
"@typespec/xml": "0.87.0",
|
|
58
61
|
"ajv": "^8.20.0",
|
|
59
62
|
"oxfmt": "^0.67.0",
|
|
60
63
|
"oxlint": "^1.82.0",
|
|
@@ -62,14 +65,14 @@
|
|
|
62
65
|
"typespec-http-zod": "link:.",
|
|
63
66
|
"vitest": "^5.0.0",
|
|
64
67
|
"yaml": "^2.9.1",
|
|
65
|
-
"zod": "^4.5
|
|
68
|
+
"zod": "^4.6.5"
|
|
66
69
|
},
|
|
67
70
|
"peerDependencies": {
|
|
68
71
|
"@typespec/compiler": "^1.15.0",
|
|
69
72
|
"@typespec/http": "^1.15.0",
|
|
70
73
|
"@typespec/openapi": "^1.15.0",
|
|
71
|
-
"@typespec/streams": "^0.85.0 || ^0.86.0",
|
|
72
|
-
"@typespec/versioning": "^0.85.0 || ^0.86.0",
|
|
74
|
+
"@typespec/streams": "^0.85.0 || ^0.86.0 || ^0.87.0",
|
|
75
|
+
"@typespec/versioning": "^0.85.0 || ^0.86.0 || ^0.87.0",
|
|
73
76
|
"zod": "^4.5.0"
|
|
74
77
|
},
|
|
75
78
|
"peerDependenciesMeta": {
|
|
@@ -85,7 +88,7 @@
|
|
|
85
88
|
},
|
|
86
89
|
"tspMain": "lib/main.tsp",
|
|
87
90
|
"scripts": {
|
|
88
|
-
"build": "tsc -p tsconfig.build.json",
|
|
91
|
+
"build": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
|
|
89
92
|
"test": "tsc -p tsconfig.build.json && vitest run",
|
|
90
93
|
"typecheck": "tsc -p tsconfig.json",
|
|
91
94
|
"lint": "oxlint src/ test/",
|