typespec-http-zod 0.10.0 → 0.11.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/dist/src/api.d.ts +40 -4
- package/dist/src/api.js +97 -7
- package/dist/src/runtime.d.ts +22 -0
- package/package.json +1 -1
package/dist/src/api.d.ts
CHANGED
|
@@ -44,11 +44,12 @@ export { EmitterOptionsSchema, type EmitterOptions } from "./lib.js";
|
|
|
44
44
|
*/
|
|
45
45
|
export { objectKey } from "./zod.js";
|
|
46
46
|
/**
|
|
47
|
-
* The
|
|
47
|
+
* The status a row registers: the first non-error status the operation declares.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
* handler can return, so it is not something this emitter can
|
|
51
|
-
* operation carrying only those is skipped rather than guessed
|
|
49
|
+
* **"Success" here means "an outcome a handler returns", which includes a redirect.** A range
|
|
50
|
+
* (`2XX`) or a wildcard is not a status a handler can return, so it is not something this emitter can
|
|
51
|
+
* turn into a `respond()` call, and an operation carrying only those is skipped rather than guessed
|
|
52
|
+
* at.
|
|
52
53
|
*/
|
|
53
54
|
export declare function successStatusOf(operation: HttpOperation): number | undefined;
|
|
54
55
|
/**
|
|
@@ -209,6 +210,41 @@ export interface EmittedRoute {
|
|
|
209
210
|
readonly status: StatusKey;
|
|
210
211
|
readonly schema: string | undefined;
|
|
211
212
|
}[];
|
|
213
|
+
/**
|
|
214
|
+
* The headers each response status declares, as the document publishes them.
|
|
215
|
+
*
|
|
216
|
+
* **`deps.respond` could not set a header the contract promises.** A spec may declare `@header` on
|
|
217
|
+
* a response model - a `Location` on a 302, a `Link`, a correlation echo - and the document
|
|
218
|
+
* publishes it under `responses.<code>.headers`, but the arm carried a status and a body schema and
|
|
219
|
+
* nothing else. So the server could not answer faithfully, and a redirect could not be served at
|
|
220
|
+
* all. Reported as blocking by a consumer, and it is a fact the document already states.
|
|
221
|
+
*
|
|
222
|
+
* **Both names are carried, because two different things need them.** `name` is the WIRE name,
|
|
223
|
+
* which is what the response sets; `property` is the name on the returned value, which is where the
|
|
224
|
+
* value is read from. `@header("x-correlation-id") correlationId: string` is `x-correlation-id` on
|
|
225
|
+
* the wire and `correlationId` in the handler's result, and an emitter given only one of them would
|
|
226
|
+
* have to guess the other.
|
|
227
|
+
*/
|
|
228
|
+
readonly responseHeaders: readonly {
|
|
229
|
+
readonly status: StatusKey;
|
|
230
|
+
readonly headers: readonly {
|
|
231
|
+
readonly name: string;
|
|
232
|
+
readonly property: string;
|
|
233
|
+
}[];
|
|
234
|
+
}[];
|
|
235
|
+
/**
|
|
236
|
+
* The media types each response status offers.
|
|
237
|
+
*
|
|
238
|
+
* **`responseContentTypes` above carries this for the PRIMARY status only, and an arm carried
|
|
239
|
+
* none at all.** An operation whose one status offers `application/json` and `text/event-stream`
|
|
240
|
+
* emitted a single arm holding the JSON schema, and the alternative was dropped from the list
|
|
241
|
+
* entirely, so `deps.respond` could not tell what it was answering with and a non-JSON operation
|
|
242
|
+
* could not be served faithfully. Reported by two consumers independently.
|
|
243
|
+
*/
|
|
244
|
+
readonly responseMediaTypes: readonly {
|
|
245
|
+
readonly status: StatusKey;
|
|
246
|
+
readonly contentTypes: readonly string[];
|
|
247
|
+
}[];
|
|
212
248
|
/**
|
|
213
249
|
* No caller is established, so the operation receives no `ServiceContext`.
|
|
214
250
|
*
|
package/dist/src/api.js
CHANGED
|
@@ -64,11 +64,12 @@ const GENERATED_BANNER = `// GENERATED by ${PACKAGE_NAME} from the TypeSpec serv
|
|
|
64
64
|
// Recompile the spec that produced it; edits here are overwritten on the next run.
|
|
65
65
|
`;
|
|
66
66
|
/**
|
|
67
|
-
* The
|
|
67
|
+
* The status a row registers: the first non-error status the operation declares.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
* handler can return, so it is not something this emitter can
|
|
71
|
-
* operation carrying only those is skipped rather than guessed
|
|
69
|
+
* **"Success" here means "an outcome a handler returns", which includes a redirect.** A range
|
|
70
|
+
* (`2XX`) or a wildcard is not a status a handler can return, so it is not something this emitter can
|
|
71
|
+
* turn into a `respond()` call, and an operation carrying only those is skipped rather than guessed
|
|
72
|
+
* at.
|
|
72
73
|
*/
|
|
73
74
|
export function successStatusOf(operation) {
|
|
74
75
|
return successStatusesOf(operation)[0];
|
|
@@ -85,7 +86,23 @@ export function successStatusesOf(operation) {
|
|
|
85
86
|
const codes = [];
|
|
86
87
|
for (const response of operation.responses) {
|
|
87
88
|
const code = response.statusCodes;
|
|
88
|
-
|
|
89
|
+
/**
|
|
90
|
+
* **2xx AND 3xx, because a redirect is an outcome rather than an error.**
|
|
91
|
+
*
|
|
92
|
+
* This accepted 2xx only, so an operation whose one declared response was a `302` had no status,
|
|
93
|
+
* was skipped by the `continue` in `collectRoutes`, and vanished with no diagnostic. The document
|
|
94
|
+
* declared the route and the emitted output had no trace of it, so a client generated from that
|
|
95
|
+
* document would 404 while the compile reported success. A `Location` on a 302 and an OAuth
|
|
96
|
+
* redirect are the ordinary cases.
|
|
97
|
+
*
|
|
98
|
+
* 4xx and 5xx stay out: those are error arms, collected from `@error` models separately, and a
|
|
99
|
+
* handler does not return one by returning normally.
|
|
100
|
+
*
|
|
101
|
+
* **Additive by construction.** Measured across the whole conformance corpus before the change:
|
|
102
|
+
* ZERO operations declare a response set without a 2xx, so no existing route, count or baseline
|
|
103
|
+
* moves. Only operations that were being dropped are added.
|
|
104
|
+
*/
|
|
105
|
+
if (typeof code === "number" && code >= 200 && code < 400)
|
|
89
106
|
codes.push(code);
|
|
90
107
|
}
|
|
91
108
|
return [...new Set(codes)].toSorted((a, b) => a - b);
|
|
@@ -261,6 +278,51 @@ function isSuccessKey(status) {
|
|
|
261
278
|
* Arms come out in the order OpenAPI resolves them - exact codes, then ranges, then the catch-all -
|
|
262
279
|
* so a consumer scanning for the first match applies the precedence the specification states.
|
|
263
280
|
*/
|
|
281
|
+
/**
|
|
282
|
+
* The headers each response status declares, keyed by the status the arm answers with.
|
|
283
|
+
*
|
|
284
|
+
* **Read from `@typespec/http`'s own resolution**, which separates a response's headers from its
|
|
285
|
+
* body and gives each one under its WIRE name with the declaring property behind it. Deriving either
|
|
286
|
+
* name any other way would be a second spelling of a fact the compiler already resolved.
|
|
287
|
+
*/
|
|
288
|
+
function responseHeadersOf(operation) {
|
|
289
|
+
const byStatus = new Map();
|
|
290
|
+
for (const response of operation.responses) {
|
|
291
|
+
const status = response.statusCodes;
|
|
292
|
+
if (typeof status !== "number")
|
|
293
|
+
continue;
|
|
294
|
+
const headers = [];
|
|
295
|
+
for (const content of response.responses) {
|
|
296
|
+
for (const [name, property] of Object.entries(content.headers ?? {})) {
|
|
297
|
+
if (!headers.some((existing) => existing.name === name)) {
|
|
298
|
+
headers.push({ name, property: property.name });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (headers.length === 0)
|
|
303
|
+
continue;
|
|
304
|
+
const existing = byStatus.get(status) ?? [];
|
|
305
|
+
byStatus.set(status, [
|
|
306
|
+
...existing,
|
|
307
|
+
...headers.filter((h) => !existing.some((e) => e.name === h.name)),
|
|
308
|
+
]);
|
|
309
|
+
}
|
|
310
|
+
return [...byStatus].map(([status, headers]) => ({ status, headers }));
|
|
311
|
+
}
|
|
312
|
+
/** The media types each numeric response status offers, from the compiler's own resolution. */
|
|
313
|
+
function responseMediaTypesOf(operation) {
|
|
314
|
+
const byStatus = new Map();
|
|
315
|
+
for (const response of operation.responses) {
|
|
316
|
+
const status = response.statusCodes;
|
|
317
|
+
if (typeof status !== "number")
|
|
318
|
+
continue;
|
|
319
|
+
const types = responseContentTypesOf(operation, status);
|
|
320
|
+
if (types.length === 0)
|
|
321
|
+
continue;
|
|
322
|
+
byStatus.set(status, types);
|
|
323
|
+
}
|
|
324
|
+
return [...byStatus].map(([status, contentTypes]) => ({ status, contentTypes }));
|
|
325
|
+
}
|
|
264
326
|
function errorArmsOf(program, operation, registry) {
|
|
265
327
|
const arms = new Map();
|
|
266
328
|
for (const response of operation.responses) {
|
|
@@ -956,6 +1018,8 @@ export function collectRoutes(program, registry, service) {
|
|
|
956
1018
|
// them under the ambient request visibility declared six components under the wrong
|
|
957
1019
|
// suffix - measured: type/model/visibility gained six divergences.
|
|
958
1020
|
errorArms: withVisibility(program, Visibility.Read, () => errorArmsOf(program, operation, registry)),
|
|
1021
|
+
responseHeaders: responseHeadersOf(operation),
|
|
1022
|
+
responseMediaTypes: responseMediaTypesOf(operation),
|
|
959
1023
|
scopes: [
|
|
960
1024
|
...new Set((getAuthenticationForOperation(program, operation.operation)?.options ?? []).flatMap((option) => option.schemes.flatMap((scheme) => scheme.type === "oauth2"
|
|
961
1025
|
? scheme.flows.flatMap((flow) => flow.scopes.map((scope) => scope.value))
|
|
@@ -1206,13 +1270,39 @@ function nameRouteSchemas(routes) {
|
|
|
1206
1270
|
function responseArmsOf(route, response, alternate) {
|
|
1207
1271
|
const arms = [];
|
|
1208
1272
|
const primary = response ?? "undefined";
|
|
1273
|
+
/**
|
|
1274
|
+
* The headers this status declares, rendered onto the arm that answers with it.
|
|
1275
|
+
*
|
|
1276
|
+
* Emitted only where there are some: an arm for a response declaring none must not acquire an
|
|
1277
|
+
* empty list, or every `respond` implementation has to tell "none declared" from "none carried".
|
|
1278
|
+
*/
|
|
1279
|
+
/**
|
|
1280
|
+
* The media types this status offers. Emitted only where the document names more than one, because
|
|
1281
|
+
* a single type is what a `respond` already assumes and repeating it on every arm in every service
|
|
1282
|
+
* would be noise rather than a fact.
|
|
1283
|
+
*/
|
|
1284
|
+
const mediaTypesFor = (status) => {
|
|
1285
|
+
const declared = route.responseMediaTypes.find((entry) => entry.status === status)?.contentTypes ?? [];
|
|
1286
|
+
if (declared.length < 2)
|
|
1287
|
+
return "";
|
|
1288
|
+
return `, contentTypes: [${declared.map((t) => JSON.stringify(t)).join(", ")}]`;
|
|
1289
|
+
};
|
|
1290
|
+
const headersFor = (status) => {
|
|
1291
|
+
const declared = route.responseHeaders.find((entry) => entry.status === status)?.headers ?? [];
|
|
1292
|
+
if (declared.length === 0)
|
|
1293
|
+
return "";
|
|
1294
|
+
const rendered = declared
|
|
1295
|
+
.map((h) => `{ name: ${JSON.stringify(h.name)}, property: ${JSON.stringify(h.property)} }`)
|
|
1296
|
+
.join(", ");
|
|
1297
|
+
return `, headers: [${rendered}]`;
|
|
1298
|
+
};
|
|
1209
1299
|
if (route.statusBy === undefined) {
|
|
1210
|
-
arms.push(`{ status: ${route.statusCode}, schema: ${primary} }`);
|
|
1300
|
+
arms.push(`{ status: ${route.statusCode}, schema: ${primary}${mediaTypesFor(route.statusCode)}${headersFor(route.statusCode)} }`);
|
|
1211
1301
|
}
|
|
1212
1302
|
else {
|
|
1213
1303
|
const alternateSchema = alternate ?? "undefined";
|
|
1214
1304
|
const { property, value, status } = route.statusBy;
|
|
1215
|
-
arms.push(`{ status: ${status}, schema: ${alternateSchema}, when: { property: ${JSON.stringify(property)}, value: ${JSON.stringify(value)} } }`, `{ status: ${route.statusCode}, schema: ${primary} }`);
|
|
1305
|
+
arms.push(`{ status: ${status}, schema: ${alternateSchema}${mediaTypesFor(status)}${headersFor(status)}, when: { property: ${JSON.stringify(property)}, value: ${JSON.stringify(value)} } }`, `{ status: ${route.statusCode}, schema: ${primary}${mediaTypesFor(route.statusCode)}${headersFor(route.statusCode)} }`);
|
|
1216
1306
|
}
|
|
1217
1307
|
/**
|
|
1218
1308
|
* The failure arms, which used to be dropped entirely.
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -39,6 +39,28 @@ import type { ZodType } from "zod";
|
|
|
39
39
|
* arm is legitimate - so the typo produces a valid-looking arm that validates nothing.
|
|
40
40
|
*/
|
|
41
41
|
export interface ResponseArm {
|
|
42
|
+
/**
|
|
43
|
+
* The media types this response offers, where the document names more than one.
|
|
44
|
+
*
|
|
45
|
+
* Absent where it names one, which is what an application already assumes. Present, it is the set
|
|
46
|
+
* to negotiate against: `selectContentType` takes the caller's `Accept` and these.
|
|
47
|
+
*/
|
|
48
|
+
readonly contentTypes?: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* The headers this response declares, as the document publishes them.
|
|
51
|
+
*
|
|
52
|
+
* **Two names, because two different things need them.** `name` is the WIRE name, which is what
|
|
53
|
+
* the response sets; `property` is the name on the value the handler returned, which is where the
|
|
54
|
+
* value is read from. `@header("x-correlation-id") correlationId: string` is `x-correlation-id`
|
|
55
|
+
* on the wire and `correlationId` in the result.
|
|
56
|
+
*
|
|
57
|
+
* Absent where the response declares none, so "none declared" and "none carried" are the same
|
|
58
|
+
* thing rather than two states an application has to tell apart.
|
|
59
|
+
*/
|
|
60
|
+
readonly headers?: readonly {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly property: string;
|
|
63
|
+
}[];
|
|
42
64
|
/**
|
|
43
65
|
* The status this arm answers with: an exact code, a `4XX`-style range, or the catch-all.
|
|
44
66
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|