typespec-http-zod 0.15.1 → 0.16.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 CHANGED
@@ -601,6 +601,29 @@ function alternateSchemaFor(operation, status, registry) {
601
601
  * document does not state AND reject what a server actually receives, since `c.req.parseBody()`
602
602
  * hands back a `File`. The type of a value is positional, exactly as its visibility is.
603
603
  */
604
+ /**
605
+ * The TS type for a **multipart** request body, built from the same parts as its validator.
606
+ *
607
+ * **`requestBodyOf` returns a type only for `bodyKind === "single"`, so a multipart body reached the
608
+ * request type not at all.** Measured: `UploadInput` was `{ "Content-Type": "multipart/form-data" }`
609
+ * while the validator beside it was `z.object({ file: ..., alt: ... })` and the document published
610
+ * every part. Two artefacts from one program, contradicting each other.
611
+ *
612
+ * **Built from `body.parts`, not from the parts model.** Walking the model types each part as its
613
+ * `HttpPart<T>` wrapper - `file: {}` - where the validator unwraps to `T`. Mirroring
614
+ * {@link multipartSchemaOf} part for part, including `multi` as an array, `optional`, and a binary
615
+ * part as `unknown`, is what makes the two agree by construction rather than by coincidence.
616
+ */
617
+ function multipartTsOf(body, registry) {
618
+ const entries = body.parts.map((part) => {
619
+ const type = part.body?.type;
620
+ const inner = type === undefined || isBinaryPart(type) ? "unknown" : registry.expressionFor(type);
621
+ const value = part.multi === true ? `(${inner})[]` : inner;
622
+ const optional = part.optional === true ? "?" : "";
623
+ return `\t${objectKey(part.name ?? "")}${optional}: ${value};`;
624
+ });
625
+ return entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
626
+ }
604
627
  function multipartSchemaOf(body, registry) {
605
628
  const entries = body.parts.map((part) => {
606
629
  const type = part.body?.type;
@@ -660,7 +683,9 @@ export function isRawBinaryMediaType(contentTypes) {
660
683
  /** The declared request body type, if the operation takes one. */
661
684
  function requestBodyOf(operation) {
662
685
  const body = operation.parameters.body;
663
- return body?.bodyKind === "single" ? body.type : undefined;
686
+ if (body?.bodyKind === "single")
687
+ return body.type;
688
+ return undefined;
664
689
  }
665
690
  /**
666
691
  * Parameters are emitted as their **own** schemas rather than folded into the body, because the two
@@ -1421,6 +1446,9 @@ function collectRequestTypes(program, registry, service) {
1421
1446
  if (successStatusOf(operation) === undefined)
1422
1447
  continue;
1423
1448
  const body = requestBodyOf(operation);
1449
+ /** The parts of a multipart body, which `requestBodyOf` deliberately does not return. */
1450
+ const multipartBody = operation.parameters.body;
1451
+ const multipart = multipartBody?.bodyKind === "multipart" ? { parts: multipartBody.parts } : undefined;
1424
1452
  /**
1425
1453
  * Headers are part of the input too.
1426
1454
  *
@@ -1466,9 +1494,11 @@ function collectRequestTypes(program, registry, service) {
1466
1494
  operationId: operationIdOf(program, operation.operation),
1467
1495
  bodyRef: rawBody !== undefined
1468
1496
  ? `{\n\t${rawBody}: ${rawBodyType};\n}`
1469
- : body === undefined
1470
- ? undefined
1471
- : registry.expressionFor(body),
1497
+ : multipart !== undefined
1498
+ ? multipartTsOf(multipart, registry)
1499
+ : body === undefined
1500
+ ? undefined
1501
+ : registry.expressionFor(body),
1472
1502
  extraProperties,
1473
1503
  });
1474
1504
  }
package/dist/src/types.js CHANGED
@@ -207,7 +207,28 @@ function modelToTs(program, model) {
207
207
  * Permissiveness is a statement about the VALIDATOR - what it lets through - not about what a
208
208
  * producer must contain. The contract type is the floor: these fields, at least.
209
209
  */
210
- return entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
210
+ /**
211
+ * **An open model carries its catchall here too, or the two walks disagree.**
212
+ *
213
+ * `modelToZod` emits `.loose()` for a `...Record<unknown>` indexer and `.catchall(T)` for a typed
214
+ * one, so `z.infer` gains an index signature. This walk emitted the declared properties alone, and
215
+ * the emitter's OWN assertion caught it:
216
+ * `TS2344: the emitted validator and the shared wire type describe different shapes`. Any spec with
217
+ * `...Record<unknown>` on a request model shipped a `wire-contract.gen.ts` that does not compile.
218
+ *
219
+ * **Emitted rather than compared away.** The document says `additionalProperties`, the validator
220
+ * says `.loose()`, so the type saying it too is what makes all three agree. Relaxing the comparison
221
+ * instead would hide the disagreement rather than remove it, and a looser comparison stops catching
222
+ * the drift it exists for.
223
+ *
224
+ * The dictionary case above returns before this: a model with an indexer and NO declared properties
225
+ * is a `Record<...>` outright rather than an object with a catchall.
226
+ */
227
+ const catchall = indexerKey === "string" && indexer !== undefined && !isNeverType(indexer.value)
228
+ ? [`\t[key: string]: ${typeToTs(program, indexer.value)};`]
229
+ : [];
230
+ const all = [...entries, ...catchall];
231
+ return all.length === 0 ? "{}" : `{\n${all.join("\n")}\n}`;
211
232
  }
212
233
  /**
213
234
  * A property line, including its optionality marker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-zod",
3
- "version": "0.15.1",
3
+ "version": "0.16.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",