typespec-http-zod 0.21.0 → 0.22.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 +14 -0
- package/dist/src/api.js +60 -5
- package/dist/src/runtime.d.ts +9 -1
- package/package.json +1 -1
package/dist/src/api.d.ts
CHANGED
|
@@ -263,6 +263,8 @@ export interface EmittedRoute {
|
|
|
263
263
|
readonly headers: readonly {
|
|
264
264
|
readonly name: string;
|
|
265
265
|
readonly property: string;
|
|
266
|
+
/** The TypeScript type of the value, so a server emitter can put it in a signature. */
|
|
267
|
+
readonly type: string;
|
|
266
268
|
}[];
|
|
267
269
|
}[];
|
|
268
270
|
/**
|
|
@@ -302,6 +304,18 @@ export interface EmittedRoute {
|
|
|
302
304
|
value: boolean | string;
|
|
303
305
|
status: number;
|
|
304
306
|
} | undefined;
|
|
307
|
+
/**
|
|
308
|
+
* The property a handler sets to choose between several declared success statuses, and the
|
|
309
|
+
* statuses it may carry - read from a `@statusCode` typed as a union of literals.
|
|
310
|
+
*
|
|
311
|
+
* **Published because the handler has to be able to SAY it.** The property is HTTP metadata, so it
|
|
312
|
+
* is stripped from the body schema (correctly - it is not body), which means a server emitter has
|
|
313
|
+
* to add it back to the type a handler returns or the arms name something unsatisfiable.
|
|
314
|
+
*/
|
|
315
|
+
readonly statusSelector: {
|
|
316
|
+
property: string;
|
|
317
|
+
statuses: readonly number[];
|
|
318
|
+
} | undefined;
|
|
305
319
|
/** The schema for the SECOND success status, when the operation declares two distinct arms. */
|
|
306
320
|
readonly alternateResponseSchema: string | undefined;
|
|
307
321
|
}
|
package/dist/src/api.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { emitFile, getDoc, getEncode, getSummary, isErrorModel, isNeverType, isVoidType, NoTarget, resolvePath, } from "@typespec/compiler";
|
|
2
|
-
import { getAuthenticationForOperation, resolveRequestVisibility, Visibility, } from "@typespec/http";
|
|
2
|
+
import { getAuthenticationForOperation, isStatusCode, resolveRequestVisibility, Visibility, } from "@typespec/http";
|
|
3
3
|
import { getOperationId, resolveOperationId } from "@typespec/openapi";
|
|
4
4
|
import { reportDiagnostic } from "./lib.js";
|
|
5
5
|
import { SchemaRegistry, TypeRegistry } from "./registry.js";
|
|
6
|
+
import { typeToTs } from "./types.js";
|
|
6
7
|
import { resolveStreamModule, streamedTypeOf, withStreamModule } from "./streams.js";
|
|
7
8
|
import { resolveVersioningModule, serviceSnapshots } from "./versioning.js";
|
|
8
9
|
import { collectExternalImports, noteDeclaredVocabularies, noteKeyVocabularies, objectKey, wireKindOf, withContractsPackage, withSealedObjects, withVisibility, } from "./zod.js";
|
|
@@ -306,7 +307,7 @@ function isSuccessKey(status) {
|
|
|
306
307
|
* body and gives each one under its WIRE name with the declaring property behind it. Deriving either
|
|
307
308
|
* name any other way would be a second spelling of a fact the compiler already resolved.
|
|
308
309
|
*/
|
|
309
|
-
function responseHeadersOf(operation) {
|
|
310
|
+
function responseHeadersOf(program, operation) {
|
|
310
311
|
const byStatus = new Map();
|
|
311
312
|
for (const response of operation.responses) {
|
|
312
313
|
const status = response.statusCodes;
|
|
@@ -316,7 +317,14 @@ function responseHeadersOf(operation) {
|
|
|
316
317
|
for (const content of response.responses) {
|
|
317
318
|
for (const [name, property] of Object.entries(content.headers ?? {})) {
|
|
318
319
|
if (!headers.some((existing) => existing.name === name)) {
|
|
319
|
-
|
|
320
|
+
/**
|
|
321
|
+
* **The TYPE too, because a handler has to be able to SET this.** A `@header`
|
|
322
|
+
* property is stripped from the body schema - correctly, it is not body - so a server
|
|
323
|
+
* emitter has to put it back into the type a handler returns, and it cannot do that
|
|
324
|
+
* from a name alone. Measured before this existed: an arm naming `contentType` and
|
|
325
|
+
* `metadata` against a handler declared `Awaitable<Result<void>>`.
|
|
326
|
+
*/
|
|
327
|
+
headers.push({ name, property: property.name, type: typeToTs(program, property.type) });
|
|
320
328
|
}
|
|
321
329
|
}
|
|
322
330
|
}
|
|
@@ -579,6 +587,36 @@ function statusDiscriminatorOf(operation, statuses) {
|
|
|
579
587
|
}
|
|
580
588
|
return undefined;
|
|
581
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* How a handler says WHICH success status it means, when the spec declares more than one.
|
|
592
|
+
*
|
|
593
|
+
* **The `@statusCode` property is the selector, and it is the one the spec already wrote.**
|
|
594
|
+
* `model Created { @statusCode statusCode: 200 | 201; @body body: Item }` declares both statuses and
|
|
595
|
+
* names the property that chooses between them, so nothing has to be inferred from the body's shape.
|
|
596
|
+
*
|
|
597
|
+
* That inference is what {@link statusDiscriminatorOf} does, and it needs a required literal property
|
|
598
|
+
* on one of the bodies. Measured across the whole conformance corpus: it fires ZERO times, so before
|
|
599
|
+
* this the second arm had never been emitted by any spec at all - `armFor` could not select a status
|
|
600
|
+
* that was never written. Both remain: a discriminator still serves two DIFFERENT response models,
|
|
601
|
+
* this serves one model declaring a union of statuses.
|
|
602
|
+
*
|
|
603
|
+
* All statuses of such a model share one body by construction - it is one model - so every arm
|
|
604
|
+
* carries the same schema and only the status and the selector value differ.
|
|
605
|
+
*/
|
|
606
|
+
function statusSelectorOf(program, operation, statuses) {
|
|
607
|
+
if (statuses.length < 2)
|
|
608
|
+
return undefined;
|
|
609
|
+
for (const response of operation.responses) {
|
|
610
|
+
const type = response.type;
|
|
611
|
+
if (type === undefined || type.kind !== "Model")
|
|
612
|
+
continue;
|
|
613
|
+
for (const [name, property] of type.properties) {
|
|
614
|
+
if (isStatusCode(program, property))
|
|
615
|
+
return { property: name, statuses };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
582
620
|
/** The emitted schema for a specific success status, used for the second arm of a dual-status op. */
|
|
583
621
|
function alternateSchemaFor(operation, status, registry) {
|
|
584
622
|
const body = successBodyOf(operation, status);
|
|
@@ -1039,6 +1077,7 @@ export function collectRoutes(program, registry, service) {
|
|
|
1039
1077
|
const requestVisibility = resolveRequestVisibility(program, operation.operation, operation.verb);
|
|
1040
1078
|
const statusCodes = successStatusesOf(operation);
|
|
1041
1079
|
const discriminator = statusDiscriminatorOf(operation, statusCodes);
|
|
1080
|
+
const statusSelector = statusSelectorOf(program, operation, statusCodes);
|
|
1042
1081
|
/**
|
|
1043
1082
|
* A `bytes` body means the BYTES are the contract.
|
|
1044
1083
|
*
|
|
@@ -1145,7 +1184,7 @@ export function collectRoutes(program, registry, service) {
|
|
|
1145
1184
|
// them under the ambient request visibility declared six components under the wrong
|
|
1146
1185
|
// suffix - measured: type/model/visibility gained six divergences.
|
|
1147
1186
|
errorArms: withVisibility(program, Visibility.Read, () => errorArmsOf(program, operation, registry)),
|
|
1148
|
-
responseHeaders: responseHeadersOf(operation),
|
|
1187
|
+
responseHeaders: responseHeadersOf(program, operation),
|
|
1149
1188
|
responseMediaTypes: responseMediaTypesOf(operation),
|
|
1150
1189
|
scopes: [
|
|
1151
1190
|
...new Set((getAuthenticationForOperation(program, operation.operation)?.options ?? []).flatMap((option) => option.schemes.flatMap((scheme) => scheme.type === "oauth2"
|
|
@@ -1154,6 +1193,7 @@ export function collectRoutes(program, registry, service) {
|
|
|
1154
1193
|
],
|
|
1155
1194
|
noAuth: noAuthFor(program, operation),
|
|
1156
1195
|
statusBy: discriminator,
|
|
1196
|
+
statusSelector,
|
|
1157
1197
|
/**
|
|
1158
1198
|
* **Each arm is validated against its OWN shape.**
|
|
1159
1199
|
*
|
|
@@ -1461,7 +1501,22 @@ function responseArmsOf(route, response, alternate) {
|
|
|
1461
1501
|
.join(", ");
|
|
1462
1502
|
return `, headers: [${rendered}]`;
|
|
1463
1503
|
};
|
|
1464
|
-
if (route.
|
|
1504
|
+
if (route.statusSelector !== undefined) {
|
|
1505
|
+
/**
|
|
1506
|
+
* **One arm per declared status, keyed on the property the spec already named.**
|
|
1507
|
+
*
|
|
1508
|
+
* The selector arms come first and the default last, the same order the discriminator below
|
|
1509
|
+
* uses: anything scanning for a match wants the specific ones before the fallback. Every arm
|
|
1510
|
+
* carries the same schema because a `@statusCode` union is ONE model - the statuses differ,
|
|
1511
|
+
* the body does not.
|
|
1512
|
+
*/
|
|
1513
|
+
const { property, statuses } = route.statusSelector;
|
|
1514
|
+
for (const status of statuses.filter((candidate) => candidate !== route.statusCode)) {
|
|
1515
|
+
arms.push(`{ status: ${status}, schema: ${primary}${mediaTypesFor(status)}${headersFor(status)}, when: { property: ${JSON.stringify(property)}, value: ${status} } }`);
|
|
1516
|
+
}
|
|
1517
|
+
arms.push(`{ status: ${route.statusCode}, schema: ${primary}${mediaTypesFor(route.statusCode)}${headersFor(route.statusCode)} }`);
|
|
1518
|
+
}
|
|
1519
|
+
else if (route.statusBy === undefined) {
|
|
1465
1520
|
arms.push(`{ status: ${route.statusCode}, schema: ${primary}${mediaTypesFor(route.statusCode)}${headersFor(route.statusCode)} }`);
|
|
1466
1521
|
}
|
|
1467
1522
|
else {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -78,9 +78,17 @@ export interface ResponseArm {
|
|
|
78
78
|
*/
|
|
79
79
|
readonly status: number | "default" | `${1 | 2 | 3 | 4 | 5}XX`;
|
|
80
80
|
readonly schema: ZodType | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* How a handler said WHICH arm it meant, where an operation declares more than one success.
|
|
83
|
+
*
|
|
84
|
+
* **`number` too, because a `@statusCode` union selects by the status itself.**
|
|
85
|
+
* `model Created { @statusCode statusCode: 200 | 201 }` names the property that chooses and its
|
|
86
|
+
* values are the statuses; the discriminator case carries a boolean or string literal off the body
|
|
87
|
+
* instead. Both answer the same question, so both use this one field.
|
|
88
|
+
*/
|
|
81
89
|
readonly when?: {
|
|
82
90
|
readonly property: string;
|
|
83
|
-
readonly value: boolean | string;
|
|
91
|
+
readonly value: boolean | number | string;
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|