typespec-http-zod 0.18.0 → 0.19.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.js +52 -4
- package/dist/src/lib.d.ts +10 -1
- package/dist/src/lib.js +13 -0
- package/dist/src/types.js +17 -1
- package/package.json +1 -1
package/dist/src/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { emitFile, getDoc, getEncode, getSummary, isErrorModel, isNeverType, isVoidType, resolvePath, } from "@typespec/compiler";
|
|
1
|
+
import { emitFile, getDoc, getEncode, getSummary, isErrorModel, isNeverType, isVoidType, NoTarget, resolvePath, } from "@typespec/compiler";
|
|
2
2
|
import { getAuthenticationForOperation, resolveRequestVisibility, Visibility, } from "@typespec/http";
|
|
3
3
|
import { getOperationId, resolveOperationId } from "@typespec/openapi";
|
|
4
4
|
import { reportDiagnostic } from "./lib.js";
|
|
@@ -1260,8 +1260,20 @@ function renderSchemas(registry, externals, routeDeclarations, runtimeModule) {
|
|
|
1260
1260
|
*
|
|
1261
1261
|
* `z.infer` of `.optional()` gives `p?: T | undefined`, which under `exactOptionalPropertyTypes`
|
|
1262
1262
|
* admits an explicitly-undefined property. **JSON has no `undefined`**, and the document says the
|
|
1263
|
-
* same thing by leaving the property out of `required`, so the inferred type
|
|
1264
|
-
*
|
|
1263
|
+
* same thing by leaving the property out of `required`, so the inferred type is wider than the
|
|
1264
|
+
* wire and a consumer reading a payload had to strip it at the boundary.
|
|
1265
|
+
*
|
|
1266
|
+
* **It does NOT match the contract type, and this docblock used to claim it did.** The
|
|
1267
|
+
* contract types in `requests.gen.ts` keep `?: T | undefined` on purpose: they are the floor a
|
|
1268
|
+
* PRODUCER supplies, and `{ p: undefined }` serialises identically to omitting `p`, so refusing
|
|
1269
|
+
* it would cost every producer a conditional spread and buy the wire nothing. Measured when this
|
|
1270
|
+
* was "fixed" to agree: `(ctx, input) => ok(input)` - return what you were given - stopped
|
|
1271
|
+
* compiling.
|
|
1272
|
+
*
|
|
1273
|
+
* So the two surfaces differ on this axis deliberately, exactly as they differ on openness: this
|
|
1274
|
+
* one is the narrow view of what ARRIVES, the contract type is the permissive floor for what you
|
|
1275
|
+
* may SEND. A reader comparing them will find them unequal and should - the emitted assertion
|
|
1276
|
+
* pairs the contract against raw `z.infer`, not against this.
|
|
1265
1277
|
*
|
|
1266
1278
|
* Derived from the same schema rather than hand-written beside it, so there is still one source of
|
|
1267
1279
|
* truth: a mapped type cannot drift from the thing it maps.
|
|
@@ -1663,8 +1675,14 @@ type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
|
1663
1675
|
* No branch for functions, deliberately: the wire vocabulary is primitives, objects and arrays, so
|
|
1664
1676
|
* a call signature cannot appear here. Mapping one would yield \`{}\`, so the day something does
|
|
1665
1677
|
* carry a method - a narrowed multipart file part is the near miss - this needs one.
|
|
1678
|
+
*
|
|
1679
|
+
* **Exported, because a consumer needs it for a NAMED type and could not reach it.** It was applied
|
|
1680
|
+
* to \`WireOutputs\` and declared locally, so a codebase whose layers hand back \`readonly T[]\` had
|
|
1681
|
+
* the producer view sitting in the file it was already importing and no way to name it. Measured on
|
|
1682
|
+
* one service: 2 contract methods returning \`readonly T[]\` and 35 readonly array properties, none
|
|
1683
|
+
* of them expressible against the published shape.
|
|
1666
1684
|
*/
|
|
1667
|
-
type Produced<T> = T extends readonly (infer Element)[]
|
|
1685
|
+
export type Produced<T> = T extends readonly (infer Element)[]
|
|
1668
1686
|
? readonly Produced<Element>[]
|
|
1669
1687
|
: T extends object
|
|
1670
1688
|
? { readonly [K in keyof T]: Produced<T[K]> }
|
|
@@ -1872,6 +1890,36 @@ export async function emitHttpZod(context, wrapper = {}) {
|
|
|
1872
1890
|
* there means a server emitter follows without knowing this rule exists.
|
|
1873
1891
|
*/
|
|
1874
1892
|
const distinctServices = new Set(snapshots.map((snapshot) => snapshot.service.namespace.name));
|
|
1893
|
+
/**
|
|
1894
|
+
* **Every service must seal the same way, because `@typespec/openapi3` has no per-service
|
|
1895
|
+
* options.**
|
|
1896
|
+
*
|
|
1897
|
+
* This option exists only so that two emitters answering the same question about the same models
|
|
1898
|
+
* give the same answer - it is stated twice on purpose, and its own docblock says to set it to
|
|
1899
|
+
* whatever openapi3 is set to. openapi3 applies ONE value to the whole program. So services that
|
|
1900
|
+
* seal differently cannot both be mirrored: whichever way openapi3 is configured, at least one
|
|
1901
|
+
* service publishes a document that disagrees with the validator generated beside it. Sealed here
|
|
1902
|
+
* and silent there refuses a payload the document permits; the reverse publishes a strictness the
|
|
1903
|
+
* runtime does not enforce.
|
|
1904
|
+
*
|
|
1905
|
+
* Reported once for the program rather than once per service, because the fault is the
|
|
1906
|
+
* DISAGREEMENT and no single service is the wrong one.
|
|
1907
|
+
*/
|
|
1908
|
+
const sealings = new Map();
|
|
1909
|
+
for (const snapshot of snapshots) {
|
|
1910
|
+
const name = snapshot.service.namespace.name;
|
|
1911
|
+
const perService = options.services?.[name];
|
|
1912
|
+
sealings.set(name, perService?.["seal-object-schemas"] ?? options["seal-object-schemas"] ?? false);
|
|
1913
|
+
}
|
|
1914
|
+
if (new Set(sealings.values()).size > 1) {
|
|
1915
|
+
const sealed = [...sealings.entries()].filter(([, seals]) => seals).map(([name]) => name);
|
|
1916
|
+
const open = [...sealings.entries()].filter(([, seals]) => !seals).map(([name]) => name);
|
|
1917
|
+
reportDiagnostic(context.program, {
|
|
1918
|
+
code: "unmirrorable-seal",
|
|
1919
|
+
target: NoTarget,
|
|
1920
|
+
format: { sealed: sealed.join(", "), open: open.join(", ") },
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1875
1923
|
for (const snapshot of snapshots) {
|
|
1876
1924
|
beginOperationIds();
|
|
1877
1925
|
const service = snapshot.service;
|
package/dist/src/lib.d.ts
CHANGED
|
@@ -125,6 +125,9 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
125
125
|
"empty-union": {
|
|
126
126
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
127
127
|
};
|
|
128
|
+
"unmirrorable-seal": {
|
|
129
|
+
readonly default: import("@typespec/compiler").CallableMessage<["sealed", "open"]>;
|
|
130
|
+
};
|
|
128
131
|
"unknown-key-vocabulary": {
|
|
129
132
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "service"]>;
|
|
130
133
|
};
|
|
@@ -138,7 +141,7 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
138
141
|
readonly default: import("@typespec/compiler").CallableMessage<["name"]>;
|
|
139
142
|
};
|
|
140
143
|
}, EmitterOptions, never>;
|
|
141
|
-
export declare const reportDiagnostic: <C extends "duplicate-declaration" | "duplicate-operation-id" | "empty-union" | "undeclared-discriminator" | "unknown-key-vocabulary" | "unsupported-default" | "unsupported-status-code-range" | "unsupported-type", M extends keyof {
|
|
144
|
+
export declare const reportDiagnostic: <C extends "duplicate-declaration" | "duplicate-operation-id" | "empty-union" | "undeclared-discriminator" | "unknown-key-vocabulary" | "unmirrorable-seal" | "unsupported-default" | "unsupported-status-code-range" | "unsupported-type", M extends keyof {
|
|
142
145
|
"unsupported-type": {
|
|
143
146
|
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why", "remedy"]>;
|
|
144
147
|
};
|
|
@@ -151,6 +154,9 @@ export declare const reportDiagnostic: <C extends "duplicate-declaration" | "dup
|
|
|
151
154
|
"empty-union": {
|
|
152
155
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
153
156
|
};
|
|
157
|
+
"unmirrorable-seal": {
|
|
158
|
+
readonly default: import("@typespec/compiler").CallableMessage<["sealed", "open"]>;
|
|
159
|
+
};
|
|
154
160
|
"unknown-key-vocabulary": {
|
|
155
161
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "service"]>;
|
|
156
162
|
};
|
|
@@ -176,6 +182,9 @@ export declare const reportDiagnostic: <C extends "duplicate-declaration" | "dup
|
|
|
176
182
|
"empty-union": {
|
|
177
183
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
178
184
|
};
|
|
185
|
+
"unmirrorable-seal": {
|
|
186
|
+
readonly default: import("@typespec/compiler").CallableMessage<["sealed", "open"]>;
|
|
187
|
+
};
|
|
179
188
|
"unknown-key-vocabulary": {
|
|
180
189
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "service"]>;
|
|
181
190
|
};
|
package/dist/src/lib.js
CHANGED
|
@@ -127,6 +127,19 @@ const diagnostics = {
|
|
|
127
127
|
* consumer can iterate a closed key set at run time; a typo would emit no tuple at all, and "the
|
|
128
128
|
* vocabulary is missing" looks exactly like "the vocabulary is empty" at the point it is used.
|
|
129
129
|
*/
|
|
130
|
+
/**
|
|
131
|
+
* Two services sealing differently, which `@typespec/openapi3` cannot mirror.
|
|
132
|
+
*
|
|
133
|
+
* An error rather than a warning, on the same footing as every other divergence here: the output
|
|
134
|
+
* is a document and a validator that contradict each other about whether a property may exist,
|
|
135
|
+
* and shipping that is worse than failing the build.
|
|
136
|
+
*/
|
|
137
|
+
"unmirrorable-seal": {
|
|
138
|
+
severity: "error",
|
|
139
|
+
messages: {
|
|
140
|
+
default: paramMessage `'seal-object-schemas' resolves to true for ${"sealed"} and false for ${"open"}, and '@typespec/openapi3' has no per-service options - it applies one value to the whole program. So one of these services would publish a document that disagrees with the validator emitted beside it. Give every service the same value, or split the surfaces into separate compiles.`,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
130
143
|
"unknown-key-vocabulary": {
|
|
131
144
|
severity: "error",
|
|
132
145
|
messages: {
|
package/dist/src/types.js
CHANGED
|
@@ -256,7 +256,23 @@ export function propertyToTs(program, property, wireName) {
|
|
|
256
256
|
*/
|
|
257
257
|
if (property.defaultValue !== undefined)
|
|
258
258
|
return `${key}: ${value};`;
|
|
259
|
-
|
|
259
|
+
/**
|
|
260
|
+
* **`?: T | undefined`, and dropping the `| undefined` was tried in `0.19.0` and reverted.**
|
|
261
|
+
*
|
|
262
|
+
* It looks wrong - JSON has no `undefined`, so a wire type saying `?: T` reads more honest, and
|
|
263
|
+
* it would make this walk agree with `Exact<z.infer<...>>` on the schemas side. **Measured, it
|
|
264
|
+
* breaks the most ordinary handler there is.** `createWidget: (ctx, input) => ok(input)` - return
|
|
265
|
+
* what you were given - stopped compiling, because what a handler RECEIVES carries
|
|
266
|
+
* `?: T | undefined` and what it would then have to SUPPLY did not.
|
|
267
|
+
*
|
|
268
|
+
* That is the difference from the index signature and from `readonly`, which look like the same
|
|
269
|
+
* class and are not: removing those two makes MORE values assignable, and removing this one makes
|
|
270
|
+
* fewer. `{ retiredOn: undefined }` serialises to `{}`, identical to omitting the key, so
|
|
271
|
+
* permitting it costs the wire nothing and refusing it costs every producer a conditional spread.
|
|
272
|
+
*
|
|
273
|
+
* The two emitted surfaces differ here on purpose, exactly as they do on openness:
|
|
274
|
+
* `schemas.gen.ts` exports the narrow received view, this walk emits the permissive floor.
|
|
275
|
+
*/
|
|
260
276
|
return property.optional ? `${key}?: ${value} | undefined;` : `${key}: ${value};`;
|
|
261
277
|
}
|
|
262
278
|
/** Installed by the registry so a named model is declared once and referenced everywhere else. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|