typespec-http-zod 0.29.0 → 0.31.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
@@ -1,17 +1,19 @@
1
- import { emitFile, getDoc, getDocData, getEncode, getLocationContext, getSummary, isErrorModel, isNeverType, isVoidType, navigateProgram, NoTarget, resolveEncodedName, resolvePath, walkPropertiesInherited, } from "@typespec/compiler";
2
- import { getAuthenticationForOperation, resolveRequestVisibility, Visibility, } from "@typespec/http";
1
+ import { emitFile, getDiscriminatedUnion, getDiscriminator, getDoc, getDocData, getEncode, getLocationContext, getSummary, getTypeName, isErrorModel, isNeverType, isVoidType, navigateProgram, NoTarget, resolvePath, } from "@typespec/compiler";
2
+ import { getAuthenticationForOperation, isOrExtendsHttpFile, Visibility, } from "@typespec/http";
3
3
  import { getOperationId, resolveOperationId } from "@typespec/openapi";
4
+ import { firstReportOf } from "./constraints.js";
4
5
  import { reportDiagnostic } from "./lib.js";
5
6
  import { literal } from "./literal.js";
6
- import { SchemaRegistry, TypeRegistry } from "./registry.js";
7
+ import { beginServiceView, parameterVisibilityOf, publishedComponents, publishedTypes, requestVisibilityOf, unreachedTypes, withServiceView, } from "./published.js";
8
+ import { isSingleObjectLiteral, SchemaRegistry, TypeRegistry } from "./registry.js";
7
9
  import { isJsonMediaType, isMediaRange, isTextualMediaType, mediaTypeEssenceOf, mediaTypeTypeOf, resultMembersOf, statusCodesOf, } from "./results.js";
8
10
  import { propertyTypeToTs } from "./types.js";
9
11
  import { resolveStreamModule, streamedTypeOf, withStreamModule } from "./streams.js";
10
12
  import { resolveVersioningModule, serviceSnapshots } from "./versioning.js";
11
- import { EXPLODED_QUERY_SOURCE, explodedQueryCall, needsExpansionDecoding, routeTemplateOf, URI_EXPANSION_SOURCE, uriExpansionCall, } from "./uri-template.js";
13
+ import { EXPLODED_QUERY_SOURCE, explodedQueryCall, eachReader, NO_READERS, needsExpansionDecoding, routeTemplateOf, URI_EXPANSION_SOURCE, uriExpansionCall, WIRE_READ_OBJECT_SOURCE, WIRE_READ_AS_SOURCE, WIRE_READ_SOURCE, WIRE_ITSELF_SOURCE, WIRE_READERS_SOURCE, wireItselfCall, wireReadObjectCall, } from "./uri-template.js";
12
14
  export { CONTENTLESS_STATUS_CODES, STATUS_CODES } from "./statuses.js";
13
15
  export { isJsonMediaType, isMediaRange, isTextualMediaType, mediaTypeEssenceOf, mediaTypeTypeOf, resultMembersOf, statusCodesOf, } from "./results.js";
14
- import { collectExternalImports, isPayloadProperty, noteDeclaredVocabularies, noteKeyVocabularies, objectKey, wireKindOf, withContractsPackage, withSealedObjects, withVisibility, } from "./zod.js";
16
+ import { collectExternalImports, encodedTypeOf, externalReferenceOf, holdsRawBytes, isExclusive, isTypelessMember, nullableInnerOf, noteDeclaredVocabularies, noteKeyVocabularies, objectKey, objectOfModel, reachesItselfThroughUnions, refuseExternalReference, refuseSelfReachingUnion, atItem, bindingIndexerOf, currentVisibility, dispatchedSubtypesOf, readsFromText, renderedVisibilityOf, rootVisibilityOf, withContractsPackage, withSealedObjects, withMetadataAnnotationsIgnored, withMultipartBinary, withParameterPosition, UNREPRESENTABLE, withVisibility, } from "./zod.js";
15
17
  /**
16
18
  * `typespec-http-zod` - Zod validators and framework-free contract types from a TypeSpec HTTP
17
19
  * service, agreeing with the OpenAPI document `@typespec/openapi3` publishes from the same source.
@@ -322,25 +324,29 @@ function resolveResponses(program, operation) {
322
324
  const entry = byStatus.get(key) ?? {
323
325
  status,
324
326
  body: undefined,
327
+ ignoresMetadata: false,
325
328
  contentTypes: new Set(),
326
329
  headers: [],
327
330
  streamed: false,
328
331
  file: false,
329
332
  };
330
333
  for (const content of response.responses) {
331
- if (entry.body === undefined && content.body?.bodyKind === "single") {
332
- entry.body = bodyTypeOf(content.body.type);
334
+ reportNeverBody(program, operation, content.body, `${key} response`);
335
+ const body = presentBody(content.body);
336
+ if (entry.body === undefined && body?.bodyKind === "single") {
337
+ entry.body = body.type;
338
+ entry.ignoresMetadata = ignoresMetadata(body);
333
339
  }
334
340
  /**
335
341
  * **An `Http.File` response's body is its contents, served as bytes.** The same reading
336
342
  * as a file request body: a media type and nothing to serialise. Read only for
337
343
  * `"single"`, every download in `type/file` declared `image/png` and was served empty.
338
344
  */
339
- if (entry.body === undefined && content.body?.bodyKind === "file") {
340
- entry.body = content.body.contents.type;
345
+ if (entry.body === undefined && body?.bodyKind === "file") {
346
+ entry.body = body.contents.type;
341
347
  entry.file = true;
342
348
  }
343
- for (const contentType of content.body?.contentTypes ?? []) {
349
+ for (const contentType of body?.contentTypes ?? []) {
344
350
  entry.contentTypes.add(contentType);
345
351
  }
346
352
  for (const [name, property] of Object.entries(content.headers ?? {})) {
@@ -359,11 +365,12 @@ function resolveResponses(program, operation) {
359
365
  return {
360
366
  status: entry.status,
361
367
  body: entry.body,
368
+ ignoresMetadata: entry.ignoresMetadata,
362
369
  contentTypes,
363
370
  headers: entry.headers,
364
371
  binary: entry.file ||
365
372
  (entry.body !== undefined &&
366
- isBinaryPart(entry.body) &&
373
+ isBinaryPart(program, entry.body) &&
367
374
  isRawBinaryMediaType(contentTypes)),
368
375
  streamed: entry.streamed,
369
376
  textual: !entry.file && entry.body !== undefined && isStringType(entry.body),
@@ -375,13 +382,17 @@ function inPrecedenceOrder(responses) {
375
382
  return responses.toSorted((a, b) => statusPrecedenceOf(a.status) - statusPrecedenceOf(b.status) ||
376
383
  statusOrderOf(a.status) - statusOrderOf(b.status));
377
384
  }
385
+ /** A response header's contract type: a header, as a parameter is - see `withParameterPosition`. */
386
+ function headerTypeOf(program, property) {
387
+ return withParameterPosition(true, () => propertyTypeToTs(program, property));
388
+ }
378
389
  /**
379
390
  * Every response the operation declares, rendered as Zod, in OpenAPI's precedence order.
380
391
  *
381
392
  * Every header is rendered before any body, and bodies in precedence order, which is the order this
382
393
  * has always declared components in: the emitted files do not move because the resolution moved.
383
394
  *
384
- * Call inside `withVisibility(program, Visibility.Read, ...)`: a response is read at `Read`, and
395
+ * Call inside `withVisibility(Visibility.Read, ...)`: a response is read at `Read`, and
385
396
  * resolving it under a request visibility declares components under the wrong suffix.
386
397
  */
387
398
  function responsesOf(program, operation, registry) {
@@ -395,7 +406,7 @@ function responsesOf(program, operation, registry) {
395
406
  * alone published `string`, so a handler satisfying its own result type was refused by its
396
407
  * own header check and the arm could not be returned without a cast.
397
408
  */
398
- type: propertyTypeToTs(program, property),
409
+ type: headerTypeOf(program, property),
399
410
  /**
400
411
  * **The Zod expression, which used to be discarded here.** Every REQUEST parameter goes
401
412
  * through `expressionForProperty` a few hundred lines below; a response header went
@@ -413,7 +424,7 @@ function responsesOf(program, operation, registry) {
413
424
  ? undefined
414
425
  : response.binary
415
426
  ? "z.unknown()"
416
- : registry.expressionFor(response.body),
427
+ : withMetadataAnnotationsIgnored(response.ignoresMetadata, () => registry.expressionFor(response.body, true)),
417
428
  contentTypes: response.contentTypes,
418
429
  headers,
419
430
  binary: response.binary,
@@ -606,6 +617,59 @@ export function errorBodyOf(program, service) {
606
617
  function bodyTypeOf(type) {
607
618
  return isVoidType(type) || isNeverType(type) ? undefined : type;
608
619
  }
620
+ /**
621
+ * **A body declared `never` is refused**, as `never-body`, once per declaration.
622
+ *
623
+ * `never` has no values, so the body cannot exist - and `@typespec/openapi3`, whose body paths test
624
+ * only for `void`, publishes it as `content: { "application/json": { schema: {} } }`: any value. A
625
+ * validator can agree with that or with the spec, not both, so the declaration is refused the way
626
+ * `undeclared-discriminator` refuses a discriminator the document cannot state. `void` says "no
627
+ * body", and openapi3 publishes it as none.
628
+ */
629
+ function reportNeverBody(program, operation, body, position) {
630
+ if (body?.bodyKind !== "single" || !isNeverType(body.type))
631
+ return;
632
+ const target = body.property ?? operation.operation;
633
+ if (!firstReportOf(program, "never-body", target))
634
+ return;
635
+ reportDiagnostic(program, {
636
+ code: "never-body",
637
+ target,
638
+ format: { operationId: operationIdOf(program, operation.operation), position },
639
+ });
640
+ }
641
+ /**
642
+ * A declared body as the wire carries it: `undefined` where it is `void` or `never`, so no media
643
+ * type is listed for it either.
644
+ *
645
+ * **`@typespec/http` keeps a body it was told is empty**, media types and all, whenever the response
646
+ * or request declares it explicitly - `@statusCode _: 409; @body body: void`. Every artefact read
647
+ * the media types off it, so a 409 with no body was published as `contentTypes:
648
+ * ["application/json"]` and `schema: undefined`, which a consumer reads as a body it cannot validate,
649
+ * and a `void` request body reached the Zod walk and refused the compile. openapi3 publishes no
650
+ * `content` for `void`. A `never` body is refused, as `never-body`, so no artefact states a body the
651
+ * document publishes as any value; see {@link reportNeverBody}.
652
+ */
653
+ function presentBody(body) {
654
+ return body?.bodyKind === "single" && bodyTypeOf(body.type) === undefined ? undefined : body;
655
+ }
656
+ /** The operation's request body, absent where it is declared `void` or `never`. */
657
+ function requestBodyParameterOf(operation) {
658
+ return presentBody(operation.parameters.body);
659
+ }
660
+ /**
661
+ * **The operation's parameters as the document publishes them: every one but a `never`.** openapi3
662
+ * leaves a parameter of type `never` out of the operation (`getParameterOrRef`), required or not and
663
+ * wherever it is, so `?a=1` beside `@query a?: never` is an extra parameter the document accepts.
664
+ * Read whole, each was validated as `z.never()`, which refused it, was typed `a?: never` in the
665
+ * handler's input, and was reported as `unsupported-type` at `<unknown location>:1:1` under a remedy
666
+ * saying a `never` could not get there. A `never` body property is dropped the same way - see
667
+ * `publishedPropertiesOf`. A `never` sharing its wire name with another parameter leaves the name
668
+ * to that one, which is the one the document publishes.
669
+ */
670
+ function publishedParametersOf(operation) {
671
+ return operation.parameters.parameters.filter((parameter) => !isNeverType(parameter.param.type));
672
+ }
609
673
  /**
610
674
  * The media types this operation's success response can be served as.
611
675
  *
@@ -623,26 +687,38 @@ function responseContentTypesOf(operation, status) {
623
687
  if (response.statusCodes !== status)
624
688
  continue;
625
689
  for (const content of response.responses) {
626
- for (const contentType of content.body?.contentTypes ?? [])
690
+ for (const contentType of presentBody(content.body)?.contentTypes ?? []) {
627
691
  types.add(contentType);
692
+ }
628
693
  }
629
694
  }
630
695
  return [...types];
631
696
  }
632
- /** The type a parameter actually crosses the wire as, after any `@encode` on the property. */
697
+ /**
698
+ * The type a parameter actually crosses the wire as, after any `@encode` on the property.
699
+ *
700
+ * **Read through `encodedTypeOf`, the rule the parameter's schema is built by.** This read
701
+ * `getEncode(...).type` directly, which for an array encoding on a query parameter is `string`: the
702
+ * schema kept the declared `int32[]`, as the document does, while the element decoder found no list
703
+ * and left `"1"` to meet `z.int32()`. Every path here that decides how a value is split, decoded or
704
+ * expanded reads this type, so none of them can disagree with the schema it feeds.
705
+ */
633
706
  function encodedParameterType(program, property) {
634
- return getEncode(program, property)?.type ?? property.type;
707
+ const type = encodedTypeOf(program, property) ?? property.type;
708
+ /**
709
+ * **A parameter cannot carry `null` on the wire, so `T | null` arrives as a `T`.** Read as the
710
+ * union, `?ids=1,2` for `ids?: int32[] | null` was never split or decoded, and a form-exploded
711
+ * `filter?: Filter | null` was never gathered, so its keys were stripped.
712
+ */
713
+ return nullableInnerOf(type) ?? type;
635
714
  }
636
715
  /**
637
- * The wire kind of an ARRAY parameter's elements, or `undefined` where it is not such an array.
716
+ * An ARRAY parameter's element type, or `undefined` where it is not such an array.
638
717
  *
639
- * `?ids=1,2,3` splits into strings, so each element needs the same decoding a lone scalar would.
718
+ * `?ids=1,2,3` splits into strings, so each element is read from text as a lone scalar is.
640
719
  */
641
- function elementKindOf(program, type) {
642
- if (!isArrayType(type) || type.kind !== "Model")
643
- return undefined;
644
- const element = type.indexer?.value;
645
- return element === undefined ? undefined : wireKindOf(program, element);
720
+ function elementOf(type) {
721
+ return isArrayType(type) && type.kind === "Model" ? type.indexer?.value : undefined;
646
722
  }
647
723
  /** Whether a type is an array - TypeSpec models one as a `Model` carrying an integer indexer. */
648
724
  function isArrayType(type) {
@@ -678,25 +754,42 @@ function isArrayType(type) {
678
754
  * {@link multipartSchemaOf} part for part, including `multi` as an array, `optional`, and a binary
679
755
  * part as `unknown`, is what makes the two agree by construction rather than by coincidence.
680
756
  */
681
- function multipartTsOf(body, registry) {
757
+ function multipartTsOf(program, body, registry) {
682
758
  const entries = body.parts.map((part) => {
683
759
  const type = part.body?.type;
684
760
  const inner = type === undefined
685
761
  ? "unknown"
686
- : isFilePart(type)
762
+ : isFilePart(program, type)
687
763
  ? MULTIPART_FILE
688
- : isBinaryPart(type)
764
+ : isBinaryPart(program, type)
689
765
  ? "unknown"
690
- : registry.expressionFor(type);
766
+ : withMetadataAnnotationsIgnored(ignoresMetadata(part.body), () => withMultipartBinary(holdsRawBytes(program, type), () => registry.expressionFor(type, true)));
691
767
  const value = part.multi === true ? `(${inner})[]` : inner;
692
768
  const optional = part.optional === true ? "?" : "";
693
769
  return `\t${objectKey(part.name ?? "")}${optional}: ${value};`;
694
770
  });
695
- return entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
771
+ const shape = entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
772
+ // Declared under the component's name, as its validator is - see `multipartSchemaOf`.
773
+ const name = multipartNameOf(body);
774
+ return name === undefined || body.type === undefined
775
+ ? shape
776
+ : registry.declareNamed(name, () => shape, body.type);
777
+ }
778
+ /** The component openapi3 publishes a multipart body as: its model's name, where it has one. */
779
+ function multipartNameOf(body) {
780
+ const named = body.type;
781
+ return named !== undefined && named.kind === "Model" && named.name !== ""
782
+ ? named.name
783
+ : undefined;
696
784
  }
697
785
  function multipartSchemaOf(program, body, registry) {
698
786
  const entries = body.parts.map((part) => {
699
787
  const type = part.body?.type;
788
+ // `.exactOptional()` for the same reason as a model property: `parseBody` yields present keys
789
+ // only, so an absent part is an absent key rather than an explicit `undefined`.
790
+ const entry = (value) => `\t${objectKey(part.name ?? "")}: ${part.optional === true ? `${value}.exactOptional()` : value},`;
791
+ if (isJsonListPart(program, part))
792
+ return entry(jsonListDecoded(jsonListOf(registry, part)));
700
793
  /**
701
794
  * **A form carries text, so a part that means a number or a boolean is decoded first**, by the
702
795
  * same rule {@link wireDecoded} applies to path, query and header values: `"0.5"` becomes
@@ -707,18 +800,31 @@ function multipartSchemaOf(program, body, registry) {
707
800
  */
708
801
  const inner = type === undefined
709
802
  ? "z.unknown()"
710
- : isFilePart(type)
803
+ : isFilePart(program, type)
711
804
  ? `z.unknown().refine(${MULTIPART_FILE_REFINEMENT})`
712
- : isBinaryPart(type)
805
+ : isBinaryPart(program, type)
713
806
  ? "z.unknown()"
714
- : isJsonPart(part)
715
- ? jsonTextDecoded(registry.expressionFor(type))
716
- : wireDecoded(registry.expressionFor(type), wireKindOf(program, type), elementKindOf(program, type));
717
- // `multi` is `HttpPart<T>[]` - the same part repeated, which arrives as an array.
718
- const value = part.multi === true ? `z.array(${inner})` : inner;
719
- // `.exactOptional()` for the same reason as a model property: `parseBody` yields present keys
720
- // only, so an absent part is an absent key rather than an explicit `undefined`.
721
- return `\t${objectKey(part.name ?? "")}: ${part.optional === true ? `${value}.exactOptional()` : value},`;
807
+ : holdsRawBytes(program, type)
808
+ ? /**
809
+ * Any value but what its other members refuse, and no text to read - unless the
810
+ * document publishes the part as JSON alone, `HttpPart<bytes | null>`, where its
811
+ * text is JSON and `x` is refused.
812
+ */
813
+ withMultipartBinary(true, () => isJsonPart(part)
814
+ ? jsonTextDecoded(registry.expressionFor(type, true))
815
+ : registry.expressionFor(type, true))
816
+ : withMetadataAnnotationsIgnored(ignoresMetadata(part.body), () => isJsonPart(part)
817
+ ? jsonTextDecoded(registry.expressionFor(type, true))
818
+ : isJsonOrTextPart(part)
819
+ ? jsonOrTextDecoded(registry.expressionFor(type, true), textPartDecoded(program, registry, type))
820
+ : textPartDecoded(program, registry, type));
821
+ if (part.multi !== true)
822
+ return entry(inner);
823
+ // `multi` is `HttpPart<T>[]` - the same part repeated, which the document publishes as a list,
824
+ // and one whose text may be JSON is that list in JSON as well - see `mayBeJsonList`.
825
+ return entry(isJsonOrTextPart(part)
826
+ ? jsonOrTextDecoded(jsonListOf(registry, part), `z.array(${inner})`)
827
+ : `z.array(${inner})`);
722
828
  });
723
829
  const shape = entries.length === 0 ? "z.object({})" : `z.object({\n${entries.join("\n")}\n})`;
724
830
  /**
@@ -726,9 +832,124 @@ function multipartSchemaOf(program, body, registry) {
726
832
  * for it. Emitted inline the schema is correct and the component still has no validator, which the
727
833
  * differential reports - rightly - as a contract the runtime cannot check.
728
834
  */
729
- const named = body.type;
730
- const name = named !== undefined && named.kind === "Model" && named.name !== "" ? named.name : undefined;
731
- return name === undefined ? shape : registry.declareNamed(name, () => shape);
835
+ const name = multipartNameOf(body);
836
+ return name === undefined || body.type === undefined
837
+ ? shape
838
+ : registry.declareNamed(name, () => shape, body.type);
839
+ }
840
+ /**
841
+ * **What a text part's value is read as: never `null`, which text does not spell**, so `T | null`
842
+ * is read as a `T`, as a parameter's is - see `encodedParameterType`. Read as the union, `h=1&h=2`
843
+ * for `int32[] | null` under `text/plain` reached `z.array(z.int32())` as `["1", "2"]` unread, and
844
+ * was refused where the document reads `[1, 2]`.
845
+ */
846
+ function asText(type) {
847
+ return nullableInnerOf(type) ?? type;
848
+ }
849
+ /** The parts read from text - see {@link EmittedRoute.textParts}. */
850
+ function textPartsOf(program, body) {
851
+ return body.parts.flatMap((part) => {
852
+ const type = part.body?.type;
853
+ if (part.name === undefined || type === undefined)
854
+ return [];
855
+ if (isFilePart(program, type) || isBinaryPart(program, type) || holdsRawBytes(program, type)) {
856
+ return [];
857
+ }
858
+ return [{ name: part.name, json: isJsonPart(part) }];
859
+ });
860
+ }
861
+ /**
862
+ * **A text part of a union holding a list is one value, or several sent as as many parts**, read as a
863
+ * URL-encoded form's field is - see `readerOf`: `int32 | int32[]` under `text/plain` is `5` sent
864
+ * once and `[1, 2]` sent as `1` and `2`, and `int32 | string[]` reads a lone `a` as the list `["a"]`.
865
+ * Read against the whole union, as one text is, the parts reached it unread and were refused where
866
+ * the document reads `[1, 2]`. Rendered as a part's schema is, where a parameter's refuses a tuple.
867
+ */
868
+ function textPartDecoded(program, registry, type) {
869
+ const declared = registry.expressionFor(type, true);
870
+ const inner = asText(type);
871
+ const forms = inner.kind === "Union" ? wireFormsOf(inner) : [];
872
+ const lists = forms.filter((each) => elementOf(each) !== undefined);
873
+ if (lists.length === 0)
874
+ return wireDecoded(program, registry, declared, inner);
875
+ const each = lists.map((list) => atItem(program, list, () => registry.expressionFor(elementOf(list), true)));
876
+ const one = forms.some((form) => elementOf(form) === undefined) ? declared : undefined;
877
+ return `z.preprocess((raw) => wireReadAs(raw, ${eachReader(each, one, declared)}), ${declared})`;
878
+ }
879
+ /**
880
+ * **A part the document publishes as a list under a media type that is not JSON alone is that many
881
+ * parts**, one per element, however the spec declared it: `HttpPart<{ @header contentType:
882
+ * "text/plain"; @body body: int32[] }>` is published exactly as `HttpPart<int32>[]` is, `{ type:
883
+ * array, items: int32 }` with `contentType: text/plain`, and text spells no list. Read as one part,
884
+ * a lone `5` reached `z.array` as `"5"` and was refused, where the same part declared repeated read
885
+ * it as `[5]`. A JSON part's text may spell the whole list as well - see {@link isJsonListPart}.
886
+ */
887
+ function repeatsAsText(part) {
888
+ const type = part.body?.type;
889
+ return (part.multi !== true &&
890
+ type !== undefined &&
891
+ !isJsonPart(part) &&
892
+ isArrayType(nullableInnerOf(type) ?? type));
893
+ }
894
+ /**
895
+ * **A JSON part the document publishes as a list is one list or its elements, however it is
896
+ * declared.** `HttpPart<int32[]>` and `HttpPart<{ @header contentType: "application/json"; @body
897
+ * body: int32 }>[]` are published alike, `{ type: array, items: int32 }` with `contentType:
898
+ * application/json`, and OAS 3.1 does not say whether such a part is sent once, as the list's JSON,
899
+ * or once per element, each element's. So the document admits both: `[1,2]` is the list, and `5`,
900
+ * or `1` and `2` sent as two parts, its elements. Read as declared, the first refused `5` and the
901
+ * two parts, and the second `[1,2]`. A file or raw bytes is no JSON to read.
902
+ */
903
+ function isJsonListPart(program, part) {
904
+ const type = part.body?.type;
905
+ return (type !== undefined &&
906
+ isJsonPart(part) &&
907
+ !isFilePart(program, type) &&
908
+ !isBinaryPart(program, type) &&
909
+ !holdsRawBytes(program, type) &&
910
+ (part.multi === true || isArrayType(nullableInnerOf(type) ?? type)));
911
+ }
912
+ /**
913
+ * Whether one text of a part the document publishes as a list may be the whole list in JSON: a JSON
914
+ * list part, or one that is text or JSON, published as a list - declared repeated, or as a list under
915
+ * a media type that is not JSON alone. See {@link isJsonListPart} and {@link repeatsAsText}.
916
+ */
917
+ function mayBeJsonList(program, part) {
918
+ return (isJsonListPart(program, part) ||
919
+ (isJsonOrTextPart(part) && (part.multi === true || repeatsAsText(part))));
920
+ }
921
+ /** The schema of the list a part is published as, read from JSON: a repeated part's, its elements'. */
922
+ function jsonListOf(registry, part) {
923
+ const declared = withMetadataAnnotationsIgnored(ignoresMetadata(part.body), () => registry.expressionFor(part.body.type, true));
924
+ return part.multi === true ? `z.array(${declared})` : declared;
925
+ }
926
+ /**
927
+ * **A repeated part sent once is the one-element list the document describes**, and a form hands it
928
+ * over as the one value: `parseBody({ all: true })` makes a list only of a repeated key, so one file
929
+ * sent to a multi-file field reached `z.array` as a file and was refused. Boxed for the request
930
+ * only - the component stays the document's schema, where a lone value is no list - and only what a
931
+ * form delivers for one part, text or a file, so a value no form sends is judged as it is.
932
+ *
933
+ * A part repeats where the document says it does: declared `HttpPart<T>[]`, or published as a list
934
+ * under a text media type - see {@link repeatsAsText}. **One whose text may be JSON is boxed only
935
+ * where its text is not the JSON list the part accepts** - see {@link mayBeJsonList}: `[1,2]` is
936
+ * that list, and `5` the one element.
937
+ */
938
+ function multipartRequestOf(program, registry, body, schema) {
939
+ const named = body.parts.filter((part) => part.name !== undefined);
940
+ const jsonOrText = named.filter((part) => mayBeJsonList(program, part));
941
+ const boxed = named.filter((part) => !mayBeJsonList(program, part) && (part.multi === true || repeatsAsText(part)));
942
+ if (boxed.length === 0 && jsonOrText.length === 0)
943
+ return schema;
944
+ const one = `typeof value === "string" || (value !== null && typeof value === "object" && !Array.isArray(value) && "arrayBuffer" in value)`;
945
+ const always = boxed.length === 0
946
+ ? ""
947
+ : ` for (const key of ${literal(boxed.map((part) => part.name))}) { const value = boxed[key]; if (${one}) boxed[key] = [value]; }`;
948
+ const lists = jsonOrText.map((part) => `[${literal(part.name)}, ${jsonListOf(registry, part)}]`);
949
+ const unlessJson = lists.length === 0
950
+ ? ""
951
+ : ` for (const [key, list] of [${lists.join(", ")}] as const) { const value = boxed[key]; if (typeof value === "string") { try { if (list.safeParse(JSON.parse(value)).success) continue; } catch { /* text, one element */ } } if (${one}) boxed[key] = [value]; }`;
952
+ return `z.preprocess((raw) => { if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw; const boxed: globalThis.Record<string, unknown> = { ...raw };${always}${unlessJson} return boxed; }, ${schema})`;
732
953
  }
733
954
  /**
734
955
  * The shape a multipart file part actually arrives as, spelled structurally.
@@ -743,7 +964,7 @@ function multipartSchemaOf(program, body, registry) {
743
964
  * of the wire assertion, and a mapped type turns a method into a function-typed property, so
744
965
  * spelling it this way on both sides is what keeps the two identical.
745
966
  */
746
- const MULTIPART_FILE = "{ name: string; type: string; arrayBuffer: () => Promise<ArrayBuffer> }";
967
+ const MULTIPART_FILE = "{ name: string; type: string; arrayBuffer: () => globalThis.Promise<ArrayBuffer> }";
747
968
  /**
748
969
  * The check that makes {@link MULTIPART_FILE} established rather than asserted.
749
970
  *
@@ -775,14 +996,9 @@ const MULTIPART_FILE_REFINEMENT = `(value): value is ${MULTIPART_FILE} => typeof
775
996
  * string. Widening this to cover `bytes` would refuse payloads that pass today, on an inference the
776
997
  * spec never made.
777
998
  */
778
- function isFilePart(type) {
779
- if (type.kind !== "Model")
780
- return false;
781
- for (let model = type; model !== undefined; model = model.baseModel) {
782
- if (model.name === "File")
783
- return true;
784
- }
785
- return false;
999
+ function isFilePart(program, type) {
1000
+ // `Http.File` as `@typespec/http` decides it - a spec's own `model File` is a JSON part.
1001
+ return type.kind === "Model" && isOrExtendsHttpFile(program, type);
786
1002
  }
787
1003
  /** `bytes`, or a model built on `File` - the parts openapi3 publishes as an empty schema. */
788
1004
  /**
@@ -803,10 +1019,413 @@ function isJsonPart(part) {
803
1019
  * string passed on, so a part whose schema admits a string still refuses malformed JSON. A value that
804
1020
  * is not text is left alone for the schema to judge.
805
1021
  */
1022
+ /**
1023
+ * **A part whose type mixes text with JSON** - `HttpPart<int32 | Address>` - which the document
1024
+ * publishes with `contentType: "text/plain, application/json"`: the client chooses per request, and
1025
+ * the form hands over the text either way. JSON is read where the text is JSON the schema accepts, and
1026
+ * the text is read as text otherwise; read as text alone, `{"city":"x"}` reached `Address` as a
1027
+ * string and was refused. Any JSON value, not an object alone: `"all"`, quoted, is the JSON string
1028
+ * `HttpPart<Nums | "all">` accepts, and ` 1` the number.
1029
+ */
1030
+ function isJsonOrTextPart(part) {
1031
+ const contentTypes = part.body?.contentTypes ?? [];
1032
+ const json = (contentType) => /^application\/(?:[\w.+-]+\+)?json$/i.test(contentType);
1033
+ return contentTypes.some(json) && !contentTypes.every(json);
1034
+ }
1035
+ /**
1036
+ * **A body sent as a URL-encoded form, read from the text it carries** - see
1037
+ * {@link EmittedRoute.formRequestSchema}. A form holds text, `n=5&b=true`, and the document's schema
1038
+ * describes the values it spells, so each field is read against its own schema as a parameter's is,
1039
+ * and a key sent more than once is the list it stands for. `only` where the form is the one media
1040
+ * type the body is declared under. `undefined` where no form is, or nothing in the body reads.
1041
+ *
1042
+ * **Each object the body can be is one reading of the form, and the body's whole schema judges
1043
+ * every reading**, as the document does: it accepts a form where some reading of its text is a value
1044
+ * of the schema it publishes. Read member by member against each member's own schema, a union was
1045
+ * rebuilt here beside the one the document publishes, and only a plain union of models ever was:
1046
+ * `DU` with no envelope, `DU | null`, `{ DA, string }`, `{ PU, DB }` and `DA | DB | DU` were
1047
+ * validated as JSON, so `kind=a&n=5` was refused, "expected number, received string", where the
1048
+ * document reads `{ kind: "a", n: 5 }`. A `@oneOf` judged member by member counted differently too:
1049
+ * `x=5` against `PA { x?: int32 }` and `PB { x?: string }` matched both, each by its own reading, and
1050
+ * was refused where each reading the document takes matches exactly one.
1051
+ */
1052
+ function formBodyOf(program, registry, operation, body, type, declared) {
1053
+ const contentTypes = body?.contentTypes ?? [];
1054
+ const form = (contentType) => mediaTypeEssenceOf(contentType) === "application/x-www-form-urlencoded";
1055
+ if (!contentTypes.some(form))
1056
+ return undefined;
1057
+ const only = contentTypes.every(form);
1058
+ const readings = formReadingsOf(program, registry, type);
1059
+ if (!readings.some(readsText))
1060
+ return undefined;
1061
+ const ambiguous = ambiguousFormKeyOf(program, type);
1062
+ if (ambiguous !== undefined) {
1063
+ reportDiagnostic(program, {
1064
+ code: "ambiguous-form-reading",
1065
+ target: ambiguous.target,
1066
+ format: {
1067
+ operation: operation.operation.name,
1068
+ union: getTypeName(ambiguous.union),
1069
+ member: getTypeName(ambiguous.member),
1070
+ other: getTypeName(ambiguous.other),
1071
+ key: ambiguous.key,
1072
+ first: ambiguous.first,
1073
+ second: ambiguous.second,
1074
+ },
1075
+ });
1076
+ return { schema: UNREPRESENTABLE, only };
1077
+ }
1078
+ /**
1079
+ * **A reading that reads nothing is the form as sent, and stays one**: `kind=c&n=5` is a `DC`
1080
+ * whose `n` is a string, and read only as `DA` reads it - `n` the number `5` - it was refused by
1081
+ * `DC | DA`, which accepts it as text.
1082
+ */
1083
+ const read = [
1084
+ ...new Set(readings.map((readers) => readsText(readers) ? `z.preprocess(${wireReadObjectCall(readers)}, ${declared})` : declared)),
1085
+ ];
1086
+ return {
1087
+ schema: read.length === 1 ? read[0] : `z.union([${read.join(", ")}])`,
1088
+ only,
1089
+ };
1090
+ }
1091
+ /**
1092
+ * **Where a `@oneOf` sent as a URL-encoded form depends on a reading no validator can choose.** The
1093
+ * document accepts a form where some reading of its text matches exactly one member: each key sent
1094
+ * once is a value its text spells - a number, a boolean, the text itself - or the one-element list of
1095
+ * one. A member reads each key as the first of those its schema accepts, and the whole union judges
1096
+ * that reading - see `formBodyOf`. That agrees with the document unless a member reads a key one way
1097
+ * that another member accepts, while accepting another reading the other refuses, and the other's
1098
+ * own reading of the form leaves a rival matching too: then which reading matches one member alone
1099
+ * depends on every other key and member. `x=5` against `M { x?: int32 | string }` and
1100
+ * `N { x?: int32 }` is `{ x: "5" }`, `M` alone, but read as `5` it matched both and was refused;
1101
+ * open, `x=all` against `A { x?: int32 | "all" }` and `B { y?: boolean }` is `{ x: ["all"] }`, `B`
1102
+ * alone. Finding that reading means trying every combination of every key's readings, exponential in
1103
+ * the keys, and a record's keys are the sender's to choose - so the body is refused instead, as
1104
+ * `ambiguous-form-reading`. Where no member tells another's readings apart, or the other's reading
1105
+ * rules every rival out by a key the rival requires - a `kind` literal - some member's reading is
1106
+ * the document's, and the body is validated. See {@link tellsApart}.
1107
+ *
1108
+ * A discriminated union is no such `oneOf`: its members' discriminators differ, so no reading
1109
+ * matches two of them. Nor is a union a form cannot be more than one object of.
1110
+ */
1111
+ function ambiguousFormKeyOf(program, type) {
1112
+ const visit = (each, via) => {
1113
+ if (each.kind !== "Union" || via.has(each))
1114
+ return undefined;
1115
+ const members = [...each.variants.values()].map((variant) => variant.type);
1116
+ if (isExclusive(program, each) && getDiscriminatedUnion(program, each)[0] === undefined) {
1117
+ const groups = members
1118
+ .map((member) => modelsOf(member, new Set()).map((model) => keyReadingsOf(program, model)))
1119
+ .filter((group) => group.length > 0);
1120
+ for (const [i, group] of groups.entries()) {
1121
+ for (const [j, other] of groups.entries()) {
1122
+ if (i === j)
1123
+ continue;
1124
+ const rivals = groups.filter((_, k) => k !== j).flat();
1125
+ for (const first of group) {
1126
+ for (const second of other) {
1127
+ const found = tellsApart(first, second, rivals);
1128
+ if (found !== undefined)
1129
+ return { union: each, ...found };
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ for (const member of members) {
1136
+ const found = visit(member, new Set([...via, each]));
1137
+ if (found !== undefined)
1138
+ return found;
1139
+ }
1140
+ return undefined;
1141
+ };
1142
+ return visit(type, new Set());
1143
+ }
1144
+ /** The objects a member of a union is: itself, or each a union of them reaches. */
1145
+ function modelsOf(type, seen) {
1146
+ if (type.kind === "Model")
1147
+ return isArrayType(type) ? [] : [type];
1148
+ if (type.kind !== "Union" || seen.has(type))
1149
+ return [];
1150
+ seen.add(type);
1151
+ return [...type.variants.values()].flatMap((variant) => modelsOf(variant.type, seen));
1152
+ }
1153
+ const SPELLED = 1;
1154
+ const ITSELF = 2;
1155
+ const SPELLED_LIST = 4;
1156
+ const ITSELF_LIST = 8;
1157
+ const NO_READING = { numeric: 0, boolean: 0, other: 0 };
1158
+ const EVERY_READING = { numeric: 15, boolean: 15, other: ITSELF | ITSELF_LIST };
1159
+ /** Each class of text, and the value a text of it spells. */
1160
+ const TEXT_CLASSES = [
1161
+ ["numeric", "number"],
1162
+ ["boolean", "boolean"],
1163
+ ["other", ""],
1164
+ ];
1165
+ /** A model's reading of a key: its own, or every undeclared key's. */
1166
+ function keyOf(readings, key) {
1167
+ return (key === undefined ? undefined : readings.keys.get(key)) ?? readings.rest;
1168
+ }
1169
+ /**
1170
+ * The reading a model's reader takes of a text of one class: the first its schema accepts - see
1171
+ * `wireRead` - or the text as sent where it reads none. `0` where the reader takes one the schema
1172
+ * refuses, which nothing here relies on.
1173
+ */
1174
+ function choiceOf(reading, kind) {
1175
+ return reading.read ? lowestBit(reading.readings[kind]) : ITSELF;
1176
+ }
1177
+ /**
1178
+ * **A key `member` reads one way where `other` accepts that reading and refuses another `member`
1179
+ * accepts** - and `other`'s own reading of the form no rival of it accepts, so the validator reads
1180
+ * the form only as the two do - see {@link ambiguousFormKeyOf}. Where every rival requires a key
1181
+ * whose reading by `other` it refuses, `other`'s reading matches `other` alone, and the form is
1182
+ * accepted as the document accepts it.
1183
+ */
1184
+ function tellsApart(member, other, rivals) {
1185
+ const rescued = rivals.every((rival) => [...rival.keys].some(([key, reading]) => reading.required &&
1186
+ TEXT_CLASSES.every(([kind]) => {
1187
+ const choice = choiceOf(keyOf(other, key), kind);
1188
+ return (reading.readings[kind] === 0 ||
1189
+ (choice !== 0 && (reading.readings[kind] & choice) === 0));
1190
+ })));
1191
+ if (rescued)
1192
+ return undefined;
1193
+ const keys = [...new Set([...member.keys.keys(), ...other.keys.keys()])];
1194
+ for (const key of [...keys, undefined]) {
1195
+ const mine = keyOf(member, key);
1196
+ const yours = keyOf(other, key);
1197
+ for (const [kind, spelled] of TEXT_CLASSES) {
1198
+ const accepted = mine.readings[kind];
1199
+ const choice = choiceOf(mine, kind) & accepted;
1200
+ const refused = accepted & ~yours.readings[kind];
1201
+ if (choice === 0 || (yours.readings[kind] & choice) === 0 || refused === 0)
1202
+ continue;
1203
+ return {
1204
+ member: member.model,
1205
+ other: other.model,
1206
+ target: mine.property ?? yours.property ?? member.model,
1207
+ key: key === undefined ? "a key neither declares" : `'${key}'`,
1208
+ first: describeReading(choice, spelled),
1209
+ second: describeReading(lowestBit(refused), spelled),
1210
+ };
1211
+ }
1212
+ }
1213
+ return undefined;
1214
+ }
1215
+ function lowestBit(mask) {
1216
+ return mask & -mask;
1217
+ }
1218
+ function describeReading(bit, spelled) {
1219
+ switch (bit) {
1220
+ case SPELLED:
1221
+ return `the ${spelled} it spells`;
1222
+ case ITSELF:
1223
+ return "itself";
1224
+ case SPELLED_LIST:
1225
+ return `the list of the ${spelled} it spells`;
1226
+ default:
1227
+ return "the list of itself";
1228
+ }
1229
+ }
1230
+ /** Each key a model declares, read as its validator holds it, and every other key's reading. */
1231
+ function keyReadingsOf(program, model) {
1232
+ return withVisibility(renderedVisibilityOf(program, model, currentVisibility()), () => {
1233
+ const object = objectOfModel(program, model);
1234
+ const keys = new Map();
1235
+ const none = { readings: NO_READING, read: false, required: false };
1236
+ if (object.kind !== "object")
1237
+ return { model, keys, rest: none };
1238
+ for (const member of object.members) {
1239
+ if (member.property === undefined)
1240
+ continue;
1241
+ keys.set(member.key, {
1242
+ readings: formReadingsOfType(program, encodedParameterType(program, member.property)),
1243
+ read: true,
1244
+ required: member.property.optional !== true,
1245
+ property: member.property,
1246
+ });
1247
+ }
1248
+ const rest = object.rest;
1249
+ return {
1250
+ model,
1251
+ keys,
1252
+ rest: rest === "sealed"
1253
+ ? none
1254
+ : typeof rest === "string"
1255
+ ? { readings: EVERY_READING, read: false, required: false }
1256
+ : rest.kind === "type"
1257
+ ? { readings: formReadingsOfType(program, rest.type), read: true, required: false }
1258
+ : rest.kind === "never"
1259
+ ? none
1260
+ : { readings: EVERY_READING, read: true, required: false },
1261
+ };
1262
+ });
1263
+ }
1264
+ /**
1265
+ * **The readings of a key's text a value of `type` accepts** - see {@link FormReadings}. A string
1266
+ * accepts any text as itself; a string literal its own text; a number or a boolean the value a text
1267
+ * spells; a list the one-element list of what its element accepts; a value of any type every
1268
+ * reading. A scalar that reads no number or boolean - a date, `bytes` - is text, as a string is.
1269
+ */
1270
+ function formReadingsOfType(program, type) {
1271
+ let numeric = 0;
1272
+ let boolean = 0;
1273
+ let other = 0;
1274
+ const seen = new Set();
1275
+ // A literal's own text, in the class of text it is.
1276
+ const literalText = (value, itself) => {
1277
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value))
1278
+ numeric |= itself;
1279
+ else if (value === "true" || value === "false")
1280
+ boolean |= itself;
1281
+ else
1282
+ other |= itself;
1283
+ };
1284
+ const visit = (each, list) => {
1285
+ if (seen.has(each))
1286
+ return;
1287
+ seen.add(each);
1288
+ const [spelled, itself] = list ? [SPELLED_LIST, ITSELF_LIST] : [SPELLED, ITSELF];
1289
+ if (each.kind === "UnionVariant")
1290
+ return visit(each.type, list);
1291
+ if (each.kind === "Union") {
1292
+ for (const variant of each.variants.values())
1293
+ visit(variant.type, list);
1294
+ return;
1295
+ }
1296
+ const element = elementOf(each);
1297
+ if (element !== undefined) {
1298
+ // A list of lists is no text a key spells.
1299
+ if (!list)
1300
+ visit(element, true);
1301
+ return;
1302
+ }
1303
+ if (isTypelessMember(program, each)) {
1304
+ numeric |= spelled | itself;
1305
+ boolean |= spelled | itself;
1306
+ other |= itself;
1307
+ return;
1308
+ }
1309
+ switch (each.kind) {
1310
+ case "String":
1311
+ return literalText(each.value, itself);
1312
+ case "Number":
1313
+ numeric |= spelled;
1314
+ return;
1315
+ case "Boolean":
1316
+ boolean |= spelled;
1317
+ return;
1318
+ case "Enum":
1319
+ for (const member of each.members.values())
1320
+ visit(member, list);
1321
+ return;
1322
+ case "EnumMember":
1323
+ if (typeof each.value === "number")
1324
+ numeric |= spelled;
1325
+ else
1326
+ literalText(each.value ?? each.name, itself);
1327
+ return;
1328
+ case "Scalar":
1329
+ if (readsFromText(program, each)) {
1330
+ if (isBooleanScalar(each))
1331
+ boolean |= spelled;
1332
+ else
1333
+ numeric |= spelled;
1334
+ }
1335
+ else {
1336
+ numeric |= itself;
1337
+ boolean |= itself;
1338
+ other |= itself;
1339
+ }
1340
+ return;
1341
+ default:
1342
+ return;
1343
+ }
1344
+ };
1345
+ visit(type, false);
1346
+ return { numeric, boolean, other };
1347
+ }
1348
+ /** Whether a scalar is `boolean`, or declared on it. */
1349
+ function isBooleanScalar(scalar) {
1350
+ for (let each = scalar; each !== undefined; each = each.baseScalar) {
1351
+ if (each.name === "boolean")
1352
+ return true;
1353
+ }
1354
+ return false;
1355
+ }
1356
+ /** Whether readers read anything, or leave every value the text it is. */
1357
+ function readsText(readers) {
1358
+ return readers.values !== null || readers.fields !== null || readers.by !== undefined;
1359
+ }
1360
+ /**
1361
+ * **Every object a URL-encoded form can stand for in a body of this type, as the readers of its
1362
+ * text**: a model's own, and each model a union reaches, a union nested in it included. A member that
1363
+ * is no object - `string` or `int32` beside a model, a list, `null` - is never what a form is, so it
1364
+ * adds no reading, and its schema refuses the object as the document's does. A form is never `null`.
1365
+ */
1366
+ function formReadingsOf(program, registry, type) {
1367
+ if (type.kind === "Model") {
1368
+ return isArrayType(type)
1369
+ ? []
1370
+ : [withParameterPosition(true, () => readersOf(program, registry, type, true))];
1371
+ }
1372
+ if (type.kind !== "Union")
1373
+ return [];
1374
+ const variants = [...type.variants.values()].map((variant) => variant.type);
1375
+ const [discriminated] = getDiscriminatedUnion(program, type);
1376
+ if (discriminated === undefined) {
1377
+ return variants.flatMap((variant) => formReadingsOf(program, registry, variant));
1378
+ }
1379
+ /**
1380
+ * **A discriminated union is read as the variant its discriminator names**, as a discriminated
1381
+ * base is - see `readersOf` - so a key two variants declare differently is read as the one sent.
1382
+ * With no envelope the variant is the object. With one, the object is its envelope, `{ kind, value
1383
+ * }`, and `value` is read as the variant: `kind=i&value=5` is `{ kind: "i", value: 5 }` where the
1384
+ * variant is an `int32`, and `value=5` under `kind=l` the list `[5]`.
1385
+ */
1386
+ const { envelope, discriminatorPropertyName, envelopePropertyName } = discriminated.options;
1387
+ const byVariant = [...discriminated.variants].map(([value, variant]) => {
1388
+ if (envelope === "none") {
1389
+ return [value, formReadingsOf(program, registry, variant)[0] ?? NO_READERS];
1390
+ }
1391
+ const reader = readerOf(program, registry, variant, undefined, true);
1392
+ return [
1393
+ value,
1394
+ reader === null ? NO_READERS : { values: null, fields: { [envelopePropertyName]: reader } },
1395
+ ];
1396
+ });
1397
+ /**
1398
+ * A variant that is a discriminated base dispatches on a discriminator of its own, which one
1399
+ * dispatch does not reach: each variant is then a reading of its own, judged by the whole union.
1400
+ */
1401
+ if (byVariant.some(([, readers]) => readers.by !== undefined)) {
1402
+ return variants.flatMap((variant) => formReadingsOf(program, registry, variant));
1403
+ }
1404
+ if (!byVariant.some(([, readers]) => readsText(readers)))
1405
+ return [NO_READERS];
1406
+ return [
1407
+ {
1408
+ values: null,
1409
+ fields: null,
1410
+ by: { property: discriminatorPropertyName, readers: Object.fromEntries(byVariant) },
1411
+ },
1412
+ ];
1413
+ }
1414
+ function jsonOrTextDecoded(declared, asText) {
1415
+ return `z.preprocess((raw) => { if (typeof raw !== "string") return raw; try { const parsed: unknown = JSON.parse(raw); if (${declared}.safeParse(parsed).success) return parsed; } catch { /* text, read as text */ } return raw; }, ${asText})`;
1416
+ }
1417
+ /**
1418
+ * **A JSON list part, decoded from one text as the whole list or from several as its elements** -
1419
+ * see {@link isJsonListPart}. A lone text is the list here, as the component publishes it; the
1420
+ * request boxes one that is not into the one element it is, before this applies.
1421
+ */
1422
+ function jsonListDecoded(declared) {
1423
+ return `z.preprocess((raw, ctx) => { try { if (typeof raw === "string") return JSON.parse(raw); if (Array.isArray(raw)) return raw.map((each: unknown) => (typeof each === "string" ? JSON.parse(each) : each)); } catch { ctx.addIssue({ code: "custom", message: "Invalid JSON in a multipart part" }); return z.NEVER; } return raw; }, ${declared})`;
1424
+ }
806
1425
  function jsonTextDecoded(declared) {
807
1426
  return `z.preprocess((raw, ctx) => { if (typeof raw !== "string") return raw; try { return JSON.parse(raw); } catch { ctx.addIssue({ code: "custom", message: "Invalid JSON in a multipart part" }); return z.NEVER; } }, ${declared})`;
808
1427
  }
809
- function isBinaryPart(type) {
1428
+ function isBinaryPart(program, type) {
810
1429
  if (type.kind === "Scalar") {
811
1430
  for (let scalar = type; scalar !== undefined; scalar = scalar.baseScalar) {
812
1431
  if (scalar.name === "bytes")
@@ -814,13 +1433,7 @@ function isBinaryPart(type) {
814
1433
  }
815
1434
  return false;
816
1435
  }
817
- if (type.kind !== "Model")
818
- return false;
819
- for (let model = type; model !== undefined; model = model.baseModel) {
820
- if (model.name === "File")
821
- return true;
822
- }
823
- return false;
1436
+ return type.kind === "Model" && isOrExtendsHttpFile(program, type);
824
1437
  }
825
1438
  /**
826
1439
  * Whether a body served under these media types is RAW BINARY rather than a JSON value.
@@ -854,9 +1467,20 @@ function isParseableMediaType(type) {
854
1467
  essence === "application/x-www-form-urlencoded" ||
855
1468
  essence.startsWith("multipart/"));
856
1469
  }
1470
+ /**
1471
+ * Whether a body is rendered with its metadata annotations ignored, as openapi3 renders it: an
1472
+ * explicit `@body` whose type carries them - see `withMetadataAnnotationsIgnored`.
1473
+ */
1474
+ function ignoresMetadata(body) {
1475
+ return (body !== undefined &&
1476
+ "isExplicit" in body &&
1477
+ body.isExplicit === true &&
1478
+ "containsMetadataAnnotations" in body &&
1479
+ body.containsMetadataAnnotations === true);
1480
+ }
857
1481
  /** The declared request body type, if the operation takes one. */
858
1482
  function requestBodyOf(operation) {
859
- const body = operation.parameters.body;
1483
+ const body = requestBodyParameterOf(operation);
860
1484
  if (body?.bodyKind === "single")
861
1485
  return body.type;
862
1486
  return undefined;
@@ -893,9 +1517,48 @@ const COLLECTION_DELIMITERS = {
893
1517
  "ArrayEncoding.commaDelimited": ",",
894
1518
  "ArrayEncoding.newlineDelimited": "\n",
895
1519
  };
896
- /** Whether the declared type is a list, which is what makes a delimiter meaningful. */
897
- function isCollectionParameter(property) {
898
- return isArrayType(property.type);
1520
+ /** A parameter's array encoding - the `style` openapi3 publishes for it - or `undefined`. */
1521
+ function arrayEncodingOf(program, property) {
1522
+ const encoding = getEncode(program, property)?.encoding;
1523
+ return encoding !== undefined && encoding in COLLECTION_DELIMITERS ? encoding : undefined;
1524
+ }
1525
+ /**
1526
+ * Whether the parameter crosses the wire as a list, which is what makes a delimiter meaningful.
1527
+ *
1528
+ * **The encoded type, not the declared one.** `@header @encode(ArrayEncoding.commaDelimited) tags:
1529
+ * string[]` is a string on the wire - the document publishes `type: string` - and its schema is
1530
+ * `z.string()`; reading the declared list split `"a,b"` into an array and handed that to the string
1531
+ * schema, so every request was refused. Only a query parameter keeps its list under an array
1532
+ * encoding, and only there is a split undone.
1533
+ */
1534
+ function isCollectionParameter(wire) {
1535
+ return isArrayType(wire);
1536
+ }
1537
+ /**
1538
+ * **The forms a parameter's text can take: one per shape its type's members have.** A union of a
1539
+ * scalar and a list - `int32 | string[]` - is `5` or `a,b` on the wire, and each is undone its own
1540
+ * way: the scalar members read as text, each list split, each record or model expanded. Undone only
1541
+ * as the scalar it might be, `a,b` was refused. A type of one shape is one form, itself.
1542
+ */
1543
+ function wireFormsOf(type) {
1544
+ if (type.kind !== "Union")
1545
+ return [type];
1546
+ const members = [];
1547
+ const visit = (each) => {
1548
+ if (each.kind === "UnionVariant")
1549
+ return visit(each.type);
1550
+ if (each.kind === "Union")
1551
+ return [...each.variants.values()].forEach((v) => visit(v.type));
1552
+ if (!(each.kind === "Intrinsic" && each.name === "null"))
1553
+ members.push(each);
1554
+ };
1555
+ visit(type);
1556
+ const shaped = members.filter((member) => expansionShapeOf(member) !== "scalar");
1557
+ if (shaped.length === 0)
1558
+ return [type];
1559
+ const scalar = members.some((member) => expansionShapeOf(member) === "scalar");
1560
+ // The union itself stands for its scalar members: a scalar's form, read against the whole schema.
1561
+ return [...(scalar ? [type] : []), ...shaped];
899
1562
  }
900
1563
  /**
901
1564
  * Whether the values arrive one per OCCURRENCE (RFC 6570 explode) rather than joined into one.
@@ -906,10 +1569,16 @@ function isCollectionParameter(property) {
906
1569
  * refused a conformant request while accepting the same list sent twice, which no document
907
1570
  * describes. Found by the first consumer's `?topics=ip` (#1).
908
1571
  */
909
- function isExplodedCollection(parameter) {
910
- return (isCollectionParameter(parameter.param) &&
911
- (parameter.type === "query" || parameter.type === "header") &&
912
- parameter.explode === true);
1572
+ function isExplodedCollection(program, parameter, wire) {
1573
+ /**
1574
+ * **A query only.** A header is `simple` style, which joins a list with commas exploded or not -
1575
+ * explode changes how a `simple` OBJECT is written, never a list - so an exploded header list
1576
+ * boxed whole was `["1,2"]`, refused where the document reads `[1, 2]`.
1577
+ */
1578
+ return (isCollectionParameter(wire) &&
1579
+ parameter.type === "query" &&
1580
+ parameter.explode === true &&
1581
+ arrayEncodingOf(program, parameter.param) === undefined);
913
1582
  }
914
1583
  /**
915
1584
  * The delimiter a parameter's values were joined with, or `undefined` when they were not joined.
@@ -918,17 +1587,24 @@ function isExplodedCollection(parameter) {
918
1587
  * `form` style, which OpenAPI joins with a comma, and a header list is `simple` style, also comma.
919
1588
  * Those are the two that looked correct because the document and the validator agreed.
920
1589
  */
921
- function collectionDelimiterOf(program, parameter) {
922
- if (!isCollectionParameter(parameter.param))
1590
+ function collectionDelimiterOf(program, parameter, wire) {
1591
+ if (!isCollectionParameter(wire))
923
1592
  return undefined;
924
- if (parameter.type !== "query" && parameter.type !== "header")
1593
+ // A cookie is `form` with `explode: false` in the document, which joins a list with commas.
1594
+ if (parameter.type !== "query" && parameter.type !== "header" && parameter.type !== "cookie") {
925
1595
  return undefined;
926
- if (parameter.explode === true)
927
- return undefined;
928
- const encoding = getEncode(program, parameter.param)?.encoding;
929
- if (encoding !== undefined && encoding in COLLECTION_DELIMITERS) {
930
- return COLLECTION_DELIMITERS[encoding];
931
1596
  }
1597
+ /**
1598
+ * **An array encoding is a `style`, and it wins over `explode`.** openapi3 writes the encoding's
1599
+ * style - `pipeDelimited` - and writes `explode: true` only by leaving it out, which for every
1600
+ * style but `form` means NOT exploded. So the document joins the list with the encoding's
1601
+ * delimiter, `?xp=1|2`, explode or not; boxed as one occurrence, that was refused.
1602
+ */
1603
+ const encoding = arrayEncodingOf(program, parameter.param);
1604
+ if (encoding !== undefined)
1605
+ return COLLECTION_DELIMITERS[encoding];
1606
+ if (parameter.type === "query" && parameter.explode === true)
1607
+ return undefined;
932
1608
  return ",";
933
1609
  }
934
1610
  /**
@@ -959,26 +1635,25 @@ function collectionDelimiterOf(program, parameter) {
959
1635
  * the document justifies. Same rule as the collection split above - decode the wire form, then apply
960
1636
  * the document's schema to the result, never instead of it.
961
1637
  *
962
- * Derived from the emitted expression rather than a restated list of scalar names: `SCALARS` in
963
- * `zod.ts` decides what becomes `z.number()`, and a second copy of that list here would stop agreeing
964
- * with it silently.
965
- */
966
- function wireDecoded(declared, kind, elementKind) {
967
- const NUMBER = '(raw) => (typeof raw === "string" && raw.trim() !== "" && Number.isFinite(Number(raw)) ? Number(raw) : raw)';
968
- const BOOLEAN = '(raw) => (raw === "true" ? true : raw === "false" ? false : raw)';
969
- if (kind === "number")
970
- return `z.preprocess(${NUMBER}, ${declared})`;
971
- if (kind === "boolean")
972
- return `z.preprocess(${BOOLEAN}, ${declared})`;
1638
+ * **Read against the schema, not by a kind decided from the type** - see `WIRE_READ_SOURCE`. A
1639
+ * kind could not say `int32 | boolean`, a numeric enum, or `int32 | "all"`, where `"5"` is `5` and
1640
+ * `"all"` is text; each of them refused a value the document accepts. Only a type with a value text
1641
+ * cannot be read at all - every string - is left without a reader, and its validator is unchanged.
1642
+ */
1643
+ function wireDecoded(program, registry, declared, type) {
1644
+ if (readsFromText(program, type)) {
1645
+ return `z.preprocess((raw) => wireRead(raw, ${declared}), ${declared})`;
1646
+ }
973
1647
  /**
974
- * A list whose ELEMENTS are numeric or boolean - `?ids=1,2,3` after the collection split, or a
975
- * repeated `?id=1&id=2`. The elements are strings for exactly the same reason the scalar is, so
976
- * the same decoder applies per element. A non-array value is left alone; the split that produces
977
- * the array runs outside this and may not have happened.
1648
+ * A list whose ELEMENTS are read from text - `?ids=1,2,3` after the collection split, or a
1649
+ * repeated `?id=1&id=2` - each against the element's own schema, as a lone scalar is. A non-array
1650
+ * value is left alone; the split that produces the array runs outside this and may not have
1651
+ * happened.
978
1652
  */
979
- if (elementKind !== undefined) {
980
- const decoder = elementKind === "number" ? NUMBER : BOOLEAN;
981
- return `z.preprocess((raw) => (Array.isArray(raw) ? raw.map(${decoder}) : raw), ${declared})`;
1653
+ const element = elementOf(type);
1654
+ if (element !== undefined && readsFromText(program, element)) {
1655
+ const schema = atItem(program, type, () => registry.expressionFor(element));
1656
+ return `z.preprocess((raw) => (Array.isArray(raw) ? raw.map((item) => wireRead(item, ${schema})) : raw), ${declared})`;
982
1657
  }
983
1658
  return declared;
984
1659
  }
@@ -1078,13 +1753,17 @@ function parameterSchemasOf(program, operation, registry, segments,
1078
1753
  /** The names of a query string written into the route itself, which no parameter may gather. */
1079
1754
  literalQueryNames) {
1080
1755
  const byTarget = {};
1756
+ const add = (target, key, expression) => {
1757
+ (byTarget[target] ??= []).push(`\t${objectKey(key)}: ${expression},`);
1758
+ };
1081
1759
  let accept;
1082
1760
  /** Form-exploded records and models, gathered back under their names around the query object. */
1083
1761
  const gathered = [];
1084
- const queryNames = operation.parameters.parameters
1762
+ const parameters = publishedParametersOf(operation);
1763
+ const queryNames = parameters
1085
1764
  .filter((parameter) => parameter.type === "query")
1086
1765
  .map((parameter) => parameter.name);
1087
- for (const parameter of operation.parameters.parameters) {
1766
+ for (const parameter of parameters) {
1088
1767
  /**
1089
1768
  * **`cookie` is here because `@typespec/http` puts it here**, and excluding it was silent.
1090
1769
  * `HttpOperationParameter` discriminates on `"header" | "cookie" | "query" | "path"`, and this
@@ -1115,110 +1794,206 @@ literalQueryNames) {
1115
1794
  * via `resolveEncodedName` - the document publishes one name per parameter, and it is this one.
1116
1795
  * The TypeSpec property name is an authoring convenience that does not survive to the contract.
1117
1796
  */
1118
- const delimiter = collectionDelimiterOf(program, parameter);
1119
- const declared = registry.expressionForProperty(parameter.param);
1120
1797
  /**
1121
- * The wire form is decoded before it is validated, never instead of it. `z.preprocess` undoes
1122
- * exactly the flattening the document's `style` describes and then applies the schema the
1123
- * document publishes, so every constraint on the list - `minItems`, the element type - still
1124
- * runs. Splitting in the handler instead would have left those unenforced.
1798
+ * **Two parameters of one location and one wire name are one value on the wire, and the
1799
+ * document publishes the first.** openapi3 writes one parameter per name and location, so
1800
+ * `@query("x") a: string` beside `@query("x") b: int32` is published as `x: string`, and `?x=abc`
1801
+ * is valid. Intersected, as both declarations, it was refused. `shared-input-key` reports it.
1125
1802
  */
1803
+ const first = parameters.find((other) => sameWireName(other, parameter));
1804
+ if (first !== parameter)
1805
+ continue;
1806
+ // A header another one names in other case is refused, as `never` - see `headerCaseCollisionOf`.
1807
+ if (headerCaseCollisionOf(program, operation, parameter)) {
1808
+ const refused = parameter.param.optional ? "z.never().exactOptional()" : "z.never()";
1809
+ add(parameter.type, parameter.name, refused);
1810
+ add("header-without-accept", parameter.name, refused);
1811
+ continue;
1812
+ }
1126
1813
  /**
1127
- * Decoded BEFORE the split is wrapped around it, so the two nest in wire order: the outer
1128
- * preprocess turns `"1,2,3"` into `["1","2","3"]`, the inner one turns those into numbers, and
1129
- * the document's schema then validates the result. Wrapping the other way round would hand the
1130
- * element decoder an unsplit string.
1814
+ * A parameter published as a reference to another document states no schema - openapi3 writes
1815
+ * the reference and nothing else - and is refused, as a type published so is.
1131
1816
  */
1132
- const decoded = parameter.type === "header" && parameter.name.toLowerCase() === "content-type"
1133
- ? mediaTypeDecoded(declared)
1134
- : wireDecoded(declared,
1817
+ const external = externalReferenceOf(parameter.param);
1818
+ if (external !== undefined) {
1819
+ const refused = refuseExternalReference(program, parameter.param, external);
1820
+ const value = parameter.param.optional ? `${refused}.exactOptional()` : refused;
1821
+ add(parameter.type, parameter.name, value);
1822
+ if (parameter.type === "header")
1823
+ add("header-without-accept", parameter.name, value);
1824
+ continue;
1825
+ }
1826
+ // At the visibility its shared component is published at - see `parameterVisibilityOf`.
1827
+ withVisibility(parameterVisibilityOf(parameter.param) ?? currentVisibility(), () => {
1828
+ const declared = registry.expressionForProperty(parameter.param);
1135
1829
  /**
1136
- * `@encode` on the PROPERTY changes what crosses the wire, so the encoded type is
1137
- * the one to read. `@encode("unixTimestamp", int32) value: utcDateTime` is a number
1138
- * on the wire and a date in the spec, and reading the declared type alone stopped
1139
- * eighteen such parameters being decoded.
1830
+ * Each form the value can take, undone its own way and validated against the WHOLE schema - see
1831
+ * `wireFormsOf`. The first form the schema accepts is the value, as the first reading `wireRead`
1832
+ * finds is.
1140
1833
  */
1141
- wireKindOf(program, encodedParameterType(program, parameter.param)), elementKindOf(program, encodedParameterType(program, parameter.param)));
1142
- /**
1143
- * The boxing half of the same principle: an exploded array's single occurrence arrives as a
1144
- * bare string, so it is boxed into the one-element array the document describes before the
1145
- * schema runs - every constraint on the list still applies to the result. Several
1146
- * occurrences arrive already an array and pass through untouched, exactly as the delimiter
1147
- * arm leaves a pre-split value alone.
1148
- */
1149
- const boxedOrSplit = isExplodedCollection(parameter)
1150
- ? `z.preprocess((raw) => (typeof raw === "string" ? [raw] : raw), ${decoded})`
1151
- : delimiter === undefined
1152
- ? decoded
1153
- : `z.preprocess((raw) => (typeof raw === "string" ? raw.split(${literal(delimiter)}) : raw), ${decoded})`;
1154
- /**
1155
- * **An RFC 6570 expansion is undone OUTSIDE everything above**, because it is the first thing the
1156
- * wire did: `array.a.b` becomes `["a", "b"]`, then the element decoder and the document's schema
1157
- * run on that. See {@link uriExpansionCall}.
1158
- */
1159
- const shape = expansionShapeOf(parameter.param.type);
1160
- const values = recordValuesOf(program, parameter.param.type);
1161
- let expression = boxedOrSplit;
1162
- if (parameter.type === "path") {
1163
- const segment = decodedSegmentOf(parameter, segments);
1164
- if (segment !== undefined) {
1165
- expression = `z.preprocess(${uriExpansionCall({ ...segment, name: parameter.name, shape, values })}, ${decoded})`;
1166
- }
1167
- }
1168
- else if (parameter.type === "query" && shape === "record") {
1169
- if (parameter.explode === true) {
1834
+ /**
1835
+ * `@encode` on the PROPERTY changes what crosses the wire, so the encoded type is the one to
1836
+ * read - see below.
1837
+ */
1838
+ const wires = wireFormsOf(encodedParameterType(program, parameter.param));
1839
+ // The members a form-exploded query gathers back under its name - see `formOf`.
1840
+ const objectForms = parameter.type === "query" && parameter.explode === true
1841
+ ? wires.filter((wire) => expansionShapeOf(wire) === "record")
1842
+ : [];
1843
+ /**
1844
+ * **A list or a scalar beside them is the parameter sent as itself**, `?x=1&x=2`, which the
1845
+ * objects' gatherer would take as a key of its own - see `WIRE_ITSELF_SOURCE`. Then, as for a
1846
+ * union of models, the query is gathered once and each member reads it its own way.
1847
+ */
1848
+ const itself = objectForms.length > 0 && wires.length > objectForms.length;
1849
+ const gatheredOnce = objectForms.length > 1 || itself;
1850
+ const gatheredOthers = [
1851
+ ...queryNames.filter((name) => name !== parameter.name),
1852
+ ...literalQueryNames,
1853
+ ];
1854
+ const formOf = (wire) => {
1855
+ const delimiter = collectionDelimiterOf(program, parameter, wire);
1856
+ /**
1857
+ * The wire form is decoded before it is validated, never instead of it. `z.preprocess` undoes
1858
+ * exactly the flattening the document's `style` describes and then applies the schema the
1859
+ * document publishes, so every constraint on the list - `minItems`, the element type - still
1860
+ * runs. Splitting in the handler instead would have left those unenforced.
1861
+ */
1862
+ /**
1863
+ * Decoded BEFORE the split is wrapped around it, so the two nest in wire order: the outer
1864
+ * preprocess turns `"1,2,3"` into `["1","2","3"]`, the inner one turns those into numbers, and
1865
+ * the document's schema then validates the result. Wrapping the other way round would hand the
1866
+ * element decoder an unsplit string.
1867
+ */
1868
+ const decoded = parameter.type === "header" && parameter.name.toLowerCase() === "content-type"
1869
+ ? mediaTypeDecoded(declared)
1870
+ : wireDecoded(program, registry, declared, wire);
1871
+ /**
1872
+ * The boxing half of the same principle: an exploded array's single occurrence arrives as a
1873
+ * bare string, so it is boxed into the one-element array the document describes before the
1874
+ * schema runs - every constraint on the list still applies to the result. Several
1875
+ * occurrences arrive already an array and pass through untouched, exactly as the delimiter
1876
+ * arm leaves a pre-split value alone.
1877
+ */
1878
+ const boxedOrSplit = isExplodedCollection(program, parameter, wire)
1879
+ ? `z.preprocess((raw) => (typeof raw === "string" ? [raw] : raw), ${decoded})`
1880
+ : delimiter === undefined
1881
+ ? decoded
1882
+ : `z.preprocess((raw) => (typeof raw === "string" ? raw.split(${literal(delimiter)}) : raw), ${decoded})`;
1170
1883
  /**
1171
- * `{?param*}` over an object expands to the object's own keys, so there is no `param` key for
1172
- * this entry to read; the gatherer around the query object puts one back.
1884
+ * **An RFC 6570 expansion is undone OUTSIDE everything above**, because it is the first thing the
1885
+ * wire did: `array.a.b` becomes `["a", "b"]`, then the element decoder and the document's schema
1886
+ * run on that. See {@link uriExpansionCall}.
1173
1887
  */
1174
- const model = parameter.param.type.kind === "Model" && parameter.param.type.indexer === undefined
1175
- ? parameter.param.type
1176
- : undefined;
1177
- const properties = model === undefined ? [] : modelPropertiesOf(model);
1888
+ const shape = expansionShapeOf(wire);
1889
+ // A key repeats only in a form-exploded query - see `readersOf`.
1890
+ const readers = readersOf(program, registry, wire, false);
1891
+ let expression = boxedOrSplit;
1892
+ if (parameter.type === "path") {
1893
+ const segment = decodedSegmentOf(program, parameter, segments, wire);
1894
+ if (segment !== undefined) {
1895
+ // A value of any type is the shape its expansion spells - see `URI_EXPANSION_SOURCE`.
1896
+ const any = shape === "scalar" && takesAnyValue(program, wire);
1897
+ expression = `z.preprocess(${uriExpansionCall({
1898
+ ...segment,
1899
+ name: parameter.name,
1900
+ shape: any ? "any" : shape,
1901
+ readers: any ? { values: "z.unknown()", fields: null } : readers,
1902
+ })}, ${decoded})`;
1903
+ }
1904
+ }
1905
+ else if ((parameter.type === "header" || parameter.type === "cookie") &&
1906
+ shape === "record") {
1907
+ /**
1908
+ * **A header or a cookie carries a record or a model as text too**, and nothing read it: the
1909
+ * validator met `"a,1,b,2"` where the document's schema describes `{a: 1, b: 2}`, and refused
1910
+ * every value. A header is `simple` style - `a,1,b,2`, or `a=1,b=2` exploded - and a cookie is
1911
+ * `form` without explode, as openapi3 publishes it: the same expansion as a query's, undone
1912
+ * the same way.
1913
+ */
1914
+ const explode = parameter.type === "header" && parameter.explode === true;
1915
+ expression = `z.preprocess(${uriExpansionCall({ prefix: "", suffix: "", operator: "", name: parameter.name, explode, shape, readers })}, ${decoded})`;
1916
+ }
1917
+ else if (parameter.type === "query" && shape === "record") {
1918
+ if (parameter.explode === true) {
1919
+ /**
1920
+ * `{?param*}` over an object expands to the object's own keys, so there is no `param` key for
1921
+ * this entry to read; the gatherer around the query object puts one back. A union of
1922
+ * models is gathered once, below, and each member reads the gathered values its own way.
1923
+ */
1924
+ const repeated = readersOf(program, registry, wire, true);
1925
+ if (gatheredOnce) {
1926
+ expression = `z.preprocess(${wireReadObjectCall(repeated)}, ${expression})`;
1927
+ }
1928
+ else {
1929
+ const model = wire.kind === "Model" && wire.indexer === undefined ? wire : undefined;
1930
+ gathered.push(explodedQueryCall({
1931
+ name: parameter.name,
1932
+ others: gatheredOthers,
1933
+ keys: model === undefined ? undefined : explodedKeysOf(program, model),
1934
+ itself: false,
1935
+ readers: repeated,
1936
+ }));
1937
+ }
1938
+ }
1939
+ else {
1940
+ expression = `z.preprocess(${uriExpansionCall({ prefix: "", suffix: "", operator: "", name: parameter.name, explode: false, shape, readers })}, ${decoded})`;
1941
+ }
1942
+ }
1943
+ else if (itself) {
1944
+ // What arrived under the name, where the objects' gatherer took the query.
1945
+ expression = `z.preprocess(${wireItselfCall()}, ${expression})`;
1946
+ }
1947
+ return expression;
1948
+ };
1949
+ /**
1950
+ * `@encode` on the PROPERTY changes what crosses the wire, so the encoded type is the one to
1951
+ * read. `@encode("unixTimestamp", int32) value: utcDateTime` is a number on the wire and a date
1952
+ * in the spec, and reading the declared type alone stopped eighteen such parameters being
1953
+ * decoded.
1954
+ */
1955
+ const forms = wires.map(formOf);
1956
+ if (gatheredOnce) {
1957
+ // One gatherer for every member, over every key one of them declares - see `formOf`.
1958
+ const keyed = objectForms.map((wire) => wire.kind === "Model" && wire.indexer === undefined
1959
+ ? explodedKeysOf(program, wire)
1960
+ : undefined);
1178
1961
  gathered.push(explodedQueryCall({
1179
1962
  name: parameter.name,
1180
- others: [...queryNames.filter((name) => name !== parameter.name), ...literalQueryNames],
1181
- keys: model === undefined
1963
+ others: gatheredOthers,
1964
+ keys: keyed.includes(undefined)
1182
1965
  ? undefined
1183
- : properties.map((property) => resolveEncodedName(program, property, "application/json")),
1184
- numbers: properties
1185
- .filter((property) => wireKindOf(program, property.type) === "number")
1186
- .map((property) => resolveEncodedName(program, property, "application/json")),
1187
- booleans: properties
1188
- .filter((property) => wireKindOf(program, property.type) === "boolean")
1189
- .map((property) => resolveEncodedName(program, property, "application/json")),
1190
- values,
1966
+ : [...new Set(keyed.flatMap((keys) => keys ?? []))],
1967
+ itself,
1968
+ readers: NO_READERS,
1191
1969
  }));
1192
1970
  }
1193
- else {
1194
- expression = `z.preprocess(${uriExpansionCall({ prefix: "", suffix: "", operator: "", name: parameter.name, explode: false, shape, values })}, ${decoded})`;
1971
+ const expression = forms.length === 1 ? forms[0] : `z.union([${forms.join(", ")}])`;
1972
+ add(parameter.type, parameter.name, expression);
1973
+ /**
1974
+ * **On a route several operations share, `accept` SELECTS the operation - it is not a
1975
+ * constraint on one of them.** Each member declares its own literal, so validating against any
1976
+ * single member's would 400 every request asking for one of the others, and 400 is the wrong
1977
+ * answer twice over: the request is well-formed, and the contract's answer to "I cannot serve
1978
+ * what you asked for" is 406. `selectContentType` decides it instead, so the negotiated route
1979
+ * validates every OTHER header and leaves this one to the negotiation.
1980
+ */
1981
+ if (parameter.type === "header" && parameter.name.toLowerCase() === "accept") {
1982
+ const declared = parameter.param.type;
1983
+ if (declared.kind === "String")
1984
+ accept = { name: parameter.name, value: declared.value };
1195
1985
  }
1196
- }
1197
- const entry = `\t${objectKey(parameter.name)}: ${expression},`;
1198
- (byTarget[parameter.type] ??= []).push(entry);
1199
- /**
1200
- * **On a route several operations share, `accept` SELECTS the operation - it is not a
1201
- * constraint on one of them.** Each member declares its own literal, so validating against any
1202
- * single member's would 400 every request asking for one of the others, and 400 is the wrong
1203
- * answer twice over: the request is well-formed, and the contract's answer to "I cannot serve
1204
- * what you asked for" is 406. `selectContentType` decides it instead, so the negotiated route
1205
- * validates every OTHER header and leaves this one to the negotiation.
1206
- */
1207
- if (parameter.type === "header" && parameter.name.toLowerCase() === "accept") {
1208
- const declared = parameter.param.type;
1209
- if (declared.kind === "String")
1210
- accept = { name: parameter.name, value: declared.value };
1211
- }
1212
- else if (parameter.type === "header") {
1213
- (byTarget["header-without-accept"] ??= []).push(entry);
1214
- }
1986
+ else if (parameter.type === "header") {
1987
+ add("header-without-accept", parameter.name, expression);
1988
+ }
1989
+ });
1215
1990
  }
1216
1991
  const fileContentType = fileContentTypeOf(operation);
1217
1992
  if (fileContentType !== undefined) {
1218
1993
  // The same decoding a declared `Content-Type` header gets: parameters dropped, lowercased.
1219
- const entry = `\t${objectKey("Content-Type")}: ${mediaTypeDecoded(registry.expressionFor(fileContentType.type))},`;
1220
- (byTarget["header"] ??= []).push(entry);
1221
- (byTarget["header-without-accept"] ??= []).push(entry);
1994
+ const expression = mediaTypeDecoded(registry.expressionFor(fileContentType.type));
1995
+ add("header", "Content-Type", expression);
1996
+ add("header-without-accept", "Content-Type", expression);
1222
1997
  }
1223
1998
  const shape = (target) => {
1224
1999
  const entries = byTarget[target];
@@ -1237,38 +2012,170 @@ literalQueryNames) {
1237
2012
  accept,
1238
2013
  };
1239
2014
  }
1240
- /** What a parameter's value decodes into once its expansion is undone. */
1241
2015
  /**
1242
2016
  * The route segment a path parameter's validator decodes, or `undefined` where it validates the
1243
2017
  * segment's text as the value. The one rule for both the validator and
1244
2018
  * {@link EmittedRoute.decodedPathParameters}, so a consumer reading the route cannot disagree with
1245
2019
  * the validator it describes.
2020
+ *
2021
+ * **Shaped from the encoded type, as the validator's schema is.** `@path
2022
+ * @encode(ArrayEncoding.commaDelimited) ids: string[]` is a string on the wire, published as `type:
2023
+ * string`, and validated by `z.string()`; shaped from the declared list it was expanded into
2024
+ * `["a", "b"]` first, and every request was refused.
1246
2025
  */
1247
- function decodedSegmentOf(parameter, segments) {
2026
+ function decodedSegmentOf(program, parameter, segments, wire) {
1248
2027
  const segment = segments.find((candidate) => candidate.kind === "expression" && candidate.parameter === parameter.name);
1249
- return segment !== undefined &&
1250
- needsExpansionDecoding(segment, expansionShapeOf(parameter.param.type))
2028
+ if (segment === undefined)
2029
+ return undefined;
2030
+ // Decoded where any form the value can take is - see `wireFormsOf`.
2031
+ const forms = wire === undefined ? wireFormsOf(encodedParameterType(program, parameter.param)) : [wire];
2032
+ return forms.some((form) => needsExpansionDecoding(segment, expansionShapeOf(form)))
1251
2033
  ? segment
1252
2034
  : undefined;
1253
2035
  }
2036
+ /**
2037
+ * **Whether a value of the type may be anything**: `unknown`, a scalar with no standard base, or a
2038
+ * union holding one - published as `{}` or with `{}` among its members - so its text may spell a list
2039
+ * or an object as well as a scalar.
2040
+ */
2041
+ function takesAnyValue(program, type) {
2042
+ if (type.kind === "UnionVariant")
2043
+ return takesAnyValue(program, type.type);
2044
+ if (type.kind === "Union") {
2045
+ return [...type.variants.values()].some((variant) => takesAnyValue(program, variant.type));
2046
+ }
2047
+ return isTypelessMember(program, type);
2048
+ }
2049
+ /** What a parameter's value decodes into once its expansion is undone. */
1254
2050
  function expansionShapeOf(type) {
1255
2051
  if (isArrayType(type))
1256
2052
  return "list";
1257
2053
  return type.kind === "Model" ? "record" : "scalar";
1258
2054
  }
1259
- /** How a record's values decode, read from its indexer the way a scalar's kind is read. */
1260
- function recordValuesOf(program, type) {
1261
- if (type.kind !== "Model" || isArrayType(type) || type.indexer === undefined)
1262
- return "";
1263
- return wireKindOf(program, type.indexer.value) ?? "";
1264
- }
1265
- /** A model's properties, its base model's first, as the document lists them. */
1266
- function modelPropertiesOf(model) {
1267
- const chain = [];
1268
- for (let current = model; current !== undefined; current = current.baseModel) {
1269
- chain.unshift(current);
2055
+ /**
2056
+ * The schemas a record's or a model's values are read from text against - see `wireRead`: every
2057
+ * value of a record against its indexer's, each key of a model against its property's. A value
2058
+ * text cannot spell anything else, a string, needs none.
2059
+ *
2060
+ * `repeats` says whether a key may arrive more than once, a list as its repeated values: a
2061
+ * form-exploded query object, and a URL-encoded form. There a lone occurrence is boxed into the list
2062
+ * it stands for. Anywhere else a value is one piece of text, which spells no list: boxed in a header,
2063
+ * a cookie, a path or a non-exploded query, `l,1` was `{ l: [1] }` where the document reads
2064
+ * `{ l: "1" }` and refuses it, and `l,1,l,2` dropped the first value.
2065
+ */
2066
+ function readersOf(program, registry, type, repeats) {
2067
+ if (type.kind !== "Model" || isArrayType(type))
2068
+ return NO_READERS;
2069
+ /**
2070
+ * **Every key no member names is read as the indexer that holds it, the topmost in the chain** -
2071
+ * see `bindingIndexerOf`. Read as the model's own, an inherited `...Record<int32>` read nothing and
2072
+ * a subtype's narrower indexer read what the base's, which the validator enforces, accepts.
2073
+ */
2074
+ const binding = bindingIndexerOf(type);
2075
+ const values = binding === undefined || isNeverType(binding.value)
2076
+ ? null
2077
+ : readerOf(program, registry, binding.value, undefined, repeats);
2078
+ /**
2079
+ * **Each key is read as the validator holds it**: the object the model is rendered as, each member
2080
+ * its most derived declaration at its position. Read first-wins from the base down, `Sub { x?:
2081
+ * string }` under `Base { x?: int32 | string }` read `"5"` as `5`, and `Sub` refused it.
2082
+ *
2083
+ * **Where an indexer reads every key, a declared key is read as its own property says** - text
2084
+ * included, so `s?: string` beside `...Record<int32 | string>` stays `"5"` rather than taking the
2085
+ * indexer's reading and becoming `5`.
2086
+ */
2087
+ const fields = {};
2088
+ withVisibility(renderedVisibilityOf(program, type, currentVisibility()), () => {
2089
+ const object = objectOfModel(program, type);
2090
+ if (object.kind !== "object")
2091
+ return;
2092
+ for (const member of object.members) {
2093
+ if (member.property === undefined)
2094
+ continue;
2095
+ const reader = readerOf(program, registry, encodedParameterType(program, member.property), { property: member.property, position: member.position }, repeats, values !== null);
2096
+ if (reader !== null)
2097
+ fields[member.key] = reader;
2098
+ }
2099
+ });
2100
+ /**
2101
+ * **A discriminated base is read as the subtype its discriminator names**: a header `Snake`
2102
+ * carrying `kind,cobra,length,5` is a `Cobra`, whose `length` is a number, and `Pet` carrying
2103
+ * `kind,cat,x,5` a `Cat`, whose `x` is a string though `Dog`'s is a number. Read by one table for
2104
+ * every subtype, a key two of them declare was read as the first's.
2105
+ */
2106
+ const discriminator = getDiscriminator(program, type);
2107
+ const dispatched = dispatchedSubtypesOf(program, type);
2108
+ return {
2109
+ values,
2110
+ fields: Object.keys(fields).length === 0 ? null : fields,
2111
+ ...(discriminator === undefined || dispatched.length === 0
2112
+ ? {}
2113
+ : {
2114
+ by: {
2115
+ property: discriminator.propertyName,
2116
+ readers: Object.fromEntries(dispatched.map(([value, subtype]) => [
2117
+ value,
2118
+ readersOf(program, registry, subtype, repeats),
2119
+ ])),
2120
+ },
2121
+ }),
2122
+ };
2123
+ }
2124
+ /**
2125
+ * The reader for one value of a record or a model, or `null` for none: a list boxed and read element
2126
+ * by element where a key repeats, a value text can spell as something else read against its schema -
2127
+ * the member's, at its position, where there is one - and text, which needs no reading unless
2128
+ * `always`.
2129
+ */
2130
+ function readerOf(program, registry, type, member, repeats, always = false) {
2131
+ const whole = () => member === undefined
2132
+ ? withParameterPosition(true, () => registry.expressionFor(type))
2133
+ : registry.expressionForProperty(member.property, member.position);
2134
+ // Text is never `null`, so a `| null` beside a list leaves the list to read.
2135
+ const inner = nullableInnerOf(type) ?? type;
2136
+ const element = elementOf(inner);
2137
+ if (element !== undefined) {
2138
+ // One piece of text spells no list, so it is left to the schema to refuse.
2139
+ if (!repeats)
2140
+ return always ? whole() : null;
2141
+ return eachReader(atItem(program, inner, () => withParameterPosition(true, () => registry.expressionFor(element))));
1270
2142
  }
1271
- return chain.flatMap((current) => [...current.properties.values()]);
2143
+ /**
2144
+ * **A union holding a list is one value or several**: `e?: int32 | int32[]` arrives as `?e=1` or
2145
+ * `?e=1&e=2`. Several are read as the list's elements; one is read against the whole type, which
2146
+ * takes it as the one value. Read as text alone, the repeated key reached the schema unread and was
2147
+ * refused where the document reads `[1, 2]`. Where a key does not repeat, it is only ever one.
2148
+ */
2149
+ const forms = inner.kind === "Union" ? wireFormsOf(inner) : [];
2150
+ const lists = forms.filter((each) => elementOf(each) !== undefined);
2151
+ if (lists.length > 0) {
2152
+ if (!repeats)
2153
+ return whole();
2154
+ /**
2155
+ * **Every list member's elements, and the whole value to choose by**: `int32[] | string[]`
2156
+ * reads `?ll=1&ll=x` as the strings, the reading the whole accepts, where its first member's
2157
+ * elements made it `[1, "x"]`, and refused. One value is the one value only where a member is
2158
+ * not a list; `?ll=x` is the list `["x"]`.
2159
+ */
2160
+ const each = lists.map((list) => atItem(program, list, () => withParameterPosition(true, () => registry.expressionFor(elementOf(list)))));
2161
+ const one = forms.some((each) => elementOf(each) === undefined) ? whole() : undefined;
2162
+ return eachReader(each, one, whole());
2163
+ }
2164
+ if (!always && !readsFromText(program, type))
2165
+ return null;
2166
+ return whole();
2167
+ }
2168
+ /**
2169
+ * **The keys a form-exploded model is gathered by: the members its validator declares**, at the
2170
+ * visibility it is rendered at. Every property was a key, so `?secret=x` on a GET gathered a
2171
+ * create-only property the document does not publish there, and a sealed model refused the request
2172
+ * the document reads as `{}`.
2173
+ */
2174
+ function explodedKeysOf(program, model) {
2175
+ return withVisibility(renderedVisibilityOf(program, model, currentVisibility()), () => {
2176
+ const object = objectOfModel(program, model);
2177
+ return object.kind === "object" ? object.members.map((member) => member.key) : [];
2178
+ });
1272
2179
  }
1273
2180
  /**
1274
2181
  * **The media type an `Http.File` body declares, as the input it is.** A file's `contentType` is the
@@ -1280,10 +2187,10 @@ function modelPropertiesOf(model) {
1280
2187
  * `undefined` where the body is not a file, or where the operation already declares the header.
1281
2188
  */
1282
2189
  function fileContentTypeOf(operation) {
1283
- const body = operation.parameters.body;
2190
+ const body = requestBodyParameterOf(operation);
1284
2191
  if (body?.bodyKind !== "file")
1285
2192
  return undefined;
1286
- const declared = operation.parameters.parameters.some((parameter) => parameter.type === "header" && parameter.name.toLowerCase() === "content-type");
2193
+ const declared = publishedParametersOf(operation).some((parameter) => parameter.type === "header" && parameter.name.toLowerCase() === "content-type");
1287
2194
  return declared ? undefined : body.contentTypeProperty;
1288
2195
  }
1289
2196
  /**
@@ -1297,25 +2204,160 @@ function fileContentTypeOf(operation) {
1297
2204
  * handler receives.
1298
2205
  */
1299
2206
  /**
1300
- * The keys a merged body would contribute to the handler's input: each property of the body SENT,
1301
- * at the operation's request visibility, under its wire name, or each named part of a multipart
1302
- * body. A read-only property is not sent, so it contributes nothing.
2207
+ * The keys a merged body would contribute to the handler's input: each key of the object the body's
2208
+ * validator declares, under its wire name, or each named part of a multipart body.
2209
+ *
2210
+ * **Read from that object, at the visibility and in the context it is rendered in**, so the two
2211
+ * cannot disagree. A read-only property is one openapi3 shares into a request that has nothing else
2212
+ * request-specific, as `readOnly` and still required under OAS 3.1's reading: read at the request
2213
+ * visibility it dropped out, so `@path accountId` beside a body requiring `accountId` was merged with
2214
+ * it and one of the two lost.
1303
2215
  */
1304
2216
  function mergedBodyKeysOf(program, operation) {
1305
- const body = operation.parameters.body;
2217
+ const body = requestBodyParameterOf(operation);
1306
2218
  if (body?.bodyKind === "multipart") {
1307
2219
  return body.parts.flatMap((part) => (part.name === undefined ? [] : [part.name]));
1308
2220
  }
1309
2221
  const type = requestBodyOf(operation);
1310
2222
  if (type?.kind !== "Model")
1311
2223
  return [];
1312
- return withVisibility(program, resolveRequestVisibility(program, operation.operation, operation.verb), () => [...walkPropertiesInherited(type)]
1313
- .filter((property) => isPayloadProperty(program, property))
1314
- .map((property) => resolveEncodedName(program, property, "application/json")));
2224
+ const requested = requestVisibilityOf(program, operation);
2225
+ return withVisibility(rootVisibilityOf(program, type, requested), () => withMetadataAnnotationsIgnored(ignoresMetadata(body), () => {
2226
+ const object = objectOfModel(program, type);
2227
+ return object.kind === "object" ? object.members.map((member) => member.key) : [];
2228
+ }));
2229
+ }
2230
+ /**
2231
+ * **Whether two parameters name one value on the wire**: one location and one name - a header's name
2232
+ * read without its case, as HTTP reads it (RFC 9110 section 5.1).
2233
+ */
2234
+ function sameWireName(one, other) {
2235
+ return (one.type === other.type &&
2236
+ (one.type === "header"
2237
+ ? one.name.toLowerCase() === other.name.toLowerCase()
2238
+ : one.name === other.name));
2239
+ }
2240
+ /**
2241
+ * **Two headers whose names differ only in case are one header**, and openapi3 publishes them as two
2242
+ * parameters of the one value - `x-foo: int32` and `X-Foo: string` - which every value has to
2243
+ * satisfy at once and a server hands over once. `header-case-collision`, an error, reported once per
2244
+ * pair, and both validate nothing; validated as the last, `X-Foo: abc` was served where the
2245
+ * document refuses it.
2246
+ */
2247
+ const headerCaseCollisionsReported = new WeakMap();
2248
+ function headerCaseCollisionOf(program, operation, parameter) {
2249
+ if (parameter.type !== "header")
2250
+ return false;
2251
+ const other = publishedParametersOf(operation).find((each) => each.type === "header" &&
2252
+ each.name !== parameter.name &&
2253
+ each.name.toLowerCase() === parameter.name.toLowerCase());
2254
+ if (other === undefined)
2255
+ return false;
2256
+ const names = [parameter.name, other.name].toSorted();
2257
+ const reported = headerCaseCollisionsReported.get(program) ?? new Set();
2258
+ headerCaseCollisionsReported.set(program, reported);
2259
+ const key = `${operation.operation.name}\u0000${names.join("\u0000")}`;
2260
+ if (!reported.has(key)) {
2261
+ reported.add(key);
2262
+ reportDiagnostic(program, {
2263
+ code: "header-case-collision",
2264
+ target: parameter.param,
2265
+ format: {
2266
+ operation: operation.operation.name,
2267
+ first: names[0] ?? "",
2268
+ second: names[1] ?? "",
2269
+ },
2270
+ });
2271
+ }
2272
+ return true;
1315
2273
  }
2274
+ const INPUT_LOCATION_NOUNS = {
2275
+ path: "path parameter",
2276
+ query: "query parameter",
2277
+ header: "header",
2278
+ cookie: "cookie",
2279
+ body: "body",
2280
+ };
2281
+ /**
2282
+ * The pairs of inputs an operation hands its handler under one key, reported once per pair and
2283
+ * program.
2284
+ *
2285
+ * **Read from the same facts the input is rendered from**: each parameter under its wire name, the
2286
+ * `Content-Type` of an `Http.File` body, and the body where {@link requestBodyPlacementOf} names it.
2287
+ * A merged body is not listed, because that decision already names the body whenever one of its keys
2288
+ * is a parameter's - which is how a `@query("body")` beside a body named `body` stays possible to
2289
+ * write, and why it is caught here rather than there.
2290
+ *
2291
+ * Resolved once per operation, since the route walk and the contract walk both read it and the
2292
+ * warning must be raised once.
2293
+ */
2294
+ function sharedInputKeysOf(program, operation) {
2295
+ const resolved = sharedInputKeys.get(operation);
2296
+ if (resolved !== undefined)
2297
+ return resolved;
2298
+ const inputs = [];
2299
+ for (const parameter of publishedParametersOf(operation)) {
2300
+ if (parameter.type === "path" ||
2301
+ parameter.type === "query" ||
2302
+ parameter.type === "header" ||
2303
+ parameter.type === "cookie") {
2304
+ inputs.push({ key: parameter.name, location: parameter.type, target: parameter.param });
2305
+ }
2306
+ }
2307
+ const fileContentType = fileContentTypeOf(operation);
2308
+ if (fileContentType !== undefined) {
2309
+ inputs.push({ key: "Content-Type", location: "header", target: fileContentType });
2310
+ }
2311
+ const placement = requestBodyPlacementOf(program, operation);
2312
+ const bodyKey = placement.rawBodyProperty ?? placement.bodyProperty;
2313
+ if (bodyKey !== undefined) {
2314
+ inputs.push({
2315
+ key: bodyKey,
2316
+ location: "body",
2317
+ target: requestBodyParameterOf(operation)?.property ?? operation.operation,
2318
+ });
2319
+ }
2320
+ const shared = [];
2321
+ inputs.forEach((input, index) => {
2322
+ const first = inputs.slice(0, index).find((earlier) => earlier.key === input.key);
2323
+ if (first === undefined)
2324
+ return;
2325
+ shared.push({ key: input.key, first: first.location, second: input.location });
2326
+ const format = {
2327
+ operation: operationIdOf(program, operation.operation),
2328
+ key: input.key,
2329
+ first: INPUT_LOCATION_NOUNS[first.location],
2330
+ second: INPUT_LOCATION_NOUNS[input.location],
2331
+ };
2332
+ /**
2333
+ * **Once per program, keyed on what the warning says.** Each emitter that runs the library
2334
+ * collects its own operations, over types a visibility or version projection may have cloned,
2335
+ * so neither the operation nor the declaration is the same object twice: `typespec-hono` beside
2336
+ * `typespec-http-mcp` raised the warning twice per pair.
2337
+ */
2338
+ const reported = sharedInputKeysReported.get(program) ?? new Set();
2339
+ sharedInputKeysReported.set(program, reported);
2340
+ const said = JSON.stringify(format);
2341
+ if (reported.has(said))
2342
+ return;
2343
+ reported.add(said);
2344
+ reportDiagnostic(program, {
2345
+ code: "shared-input-key",
2346
+ // One location, one value on the wire: the document publishes the first declaration.
2347
+ messageId: first.location === input.location ? "repeated" : "default",
2348
+ target: input.target,
2349
+ format,
2350
+ });
2351
+ });
2352
+ sharedInputKeys.set(operation, shared);
2353
+ return shared;
2354
+ }
2355
+ const sharedInputKeys = new WeakMap();
2356
+ const sharedInputKeysReported = new WeakMap();
1316
2357
  function requestBodyPlacementOf(program, operation) {
2358
+ reportNeverBody(program, operation, operation.parameters.body, "request");
1317
2359
  const requestType = requestBodyOf(operation);
1318
- const bodyParameter = operation.parameters.body;
2360
+ const bodyParameter = requestBodyParameterOf(operation);
1319
2361
  const contentTypes = [...(bodyParameter?.contentTypes ?? [])];
1320
2362
  /**
1321
2363
  * A `bytes` body means the BYTES are the contract.
@@ -1323,7 +2365,7 @@ function requestBodyPlacementOf(program, operation) {
1323
2365
  * A webhook's MAC covers exactly what arrived, so a parsed-and-re-serialised object would
1324
2366
  * verify a different string and every delivery would fail authentication.
1325
2367
  */
1326
- const bytes = requestType?.kind === "Scalar" && requestType.name === "bytes";
2368
+ const bytes = requestType?.kind === "Scalar" && program.checker.isStdType(requestType, "bytes");
1327
2369
  /**
1328
2370
  * **A body declared only under media types nothing can parse into it is handed over raw.** A
1329
2371
  * model as `application/xml` has no serialisation this emitter can derive, and the server has no
@@ -1408,10 +2450,11 @@ function requestBodyPlacementOf(program, operation) {
1408
2450
  * other.** `PUT /renames/{name}` with a body `{ name }` handed the handler one `name`, the body's,
1409
2451
  * and the path's was lost with nothing reported - in `parameters/spread`'s composite request, and
1410
2452
  * in any spec whose body repeats a path or query parameter. The document states the two
1411
- * separately; named, the handler is given both. A read-only property is not in the body sent, so
1412
- * `@path id` beside a model whose `id` is read-only collides with nothing and still merges.
2453
+ * separately; named, the handler is given both. The body's keys are the keys its validator
2454
+ * declares - see `mergedBodyKeysOf` - so a read-only `id` shared into the request, still required
2455
+ * under OAS 3.1's reading, collides with `@path id` as any other key does.
1413
2456
  */
1414
- const parameterNames = new Set(operation.parameters.parameters.map((parameter) => parameter.name));
2457
+ const parameterNames = new Set(publishedParametersOf(operation).map((parameter) => parameter.name));
1415
2458
  const collides = rawBodyProperty === undefined &&
1416
2459
  mergedBodyKeysOf(program, operation).some((key) => parameterNames.has(key));
1417
2460
  const bodyProperty = (requestType?.kind === "Model" && (requestType.indexer !== undefined || optionalBody)) ||
@@ -1443,6 +2486,9 @@ function requestBodyPlacementOf(program, operation) {
1443
2486
  };
1444
2487
  }
1445
2488
  export function collectRoutes(program, registry, service) {
2489
+ return withServiceView(program, service.namespace, () => collectServiceRoutes(program, registry, service));
2490
+ }
2491
+ function collectServiceRoutes(program, registry, service) {
1446
2492
  const routes = [];
1447
2493
  {
1448
2494
  for (const operation of service.operations) {
@@ -1466,14 +2512,16 @@ export function collectRoutes(program, registry, service) {
1466
2512
  */
1467
2513
  const streamed = streamedTypeOf(program, operation.operation.returnType);
1468
2514
  if (streamed !== undefined) {
1469
- withVisibility(program, Visibility.Read, () => registry.expressionFor(streamed));
2515
+ withVisibility(Visibility.Read, () => registry.expressionFor(streamed));
1470
2516
  }
1471
2517
  // `@requestVisibility` when the operation states one, otherwise the verb's default - the
1472
2518
  // same resolution `@typespec/openapi3` performs before emitting a request schema.
1473
- const requestVisibility = resolveRequestVisibility(program, operation.operation, operation.verb);
2519
+ const requestVisibility = requestVisibilityOf(program, operation);
1474
2520
  const { rawBodyProperty, rawBodyBinary, rawBodyUnvalidated, bodyProperty, optionalBody } = requestBodyPlacementOf(program, operation);
1475
2521
  const authentication = authenticationFor(program, operation);
1476
- const template = routeTemplateOf(operation.uriTemplate, operation.parameters.parameters
2522
+ const template = routeTemplateOf(operation.uriTemplate,
2523
+ // Every path parameter, a `never` included: the template still holds its expression.
2524
+ operation.parameters.parameters
1477
2525
  .filter((parameter) => parameter.type === "path")
1478
2526
  .map((parameter) => ({
1479
2527
  name: parameter.name,
@@ -1506,18 +2554,19 @@ export function collectRoutes(program, registry, service) {
1506
2554
  .filter((parameter) => parameter.type === "path" && parameter.allowReserved)
1507
2555
  .map((parameter) => parameter.name),
1508
2556
  pathSegments: template.segments,
1509
- decodedPathParameters: operation.parameters.parameters
1510
- .filter((parameter) => decodedSegmentOf(parameter, template.segments) !== undefined)
2557
+ sharedInputKeys: sharedInputKeysOf(program, operation),
2558
+ decodedPathParameters: publishedParametersOf(operation)
2559
+ .filter((parameter) => decodedSegmentOf(program, parameter, template.segments) !== undefined)
1511
2560
  .map((parameter) => parameter.name),
1512
2561
  literalQuery: template.literalQuery,
1513
2562
  responseContentTypes: statusCode === undefined ? [] : responseContentTypesOf(operation, statusCode),
1514
- requestContentTypes: [...(operation.parameters.body?.contentTypes ?? [])],
2563
+ requestContentTypes: [...(requestBodyParameterOf(operation)?.contentTypes ?? [])],
1515
2564
  // `isRawBinaryMediaType` is "every declared type is non-JSON", which is the same question
1516
2565
  // here: a string body under `application/json` IS json, and its reader is the JSON one.
1517
2566
  requestTextual: requestType !== undefined &&
1518
2567
  rawBodyProperty === undefined &&
1519
2568
  isStringType(requestType) &&
1520
- isRawBinaryMediaType([...(operation.parameters.body?.contentTypes ?? [])]),
2569
+ isRawBinaryMediaType([...(requestBodyParameterOf(operation)?.contentTypes ?? [])]),
1521
2570
  /**
1522
2571
  * `@summary` first, `@doc` as the fallback.
1523
2572
  *
@@ -1534,17 +2583,28 @@ export function collectRoutes(program, registry, service) {
1534
2583
  * one and not the other, and `@visibility` can add or remove properties outright. Walking
1535
2584
  * both at one visibility gives whichever was seen first to both.
1536
2585
  */
1537
- requestSchema: withVisibility(program, requestVisibility, () => {
2586
+ ...withVisibility(requestVisibility, () => {
1538
2587
  // Multipart first: its body has no single type to walk, and skipping it left every
1539
2588
  // such route mounted with nothing checking the request at all.
1540
- const parameterBody = operation.parameters.body;
2589
+ const parameterBody = requestBodyParameterOf(operation);
1541
2590
  if (parameterBody?.bodyKind === "multipart") {
1542
- return multipartSchemaOf(program, parameterBody, registry);
2591
+ return {
2592
+ requestSchema: multipartRequestOf(program, registry, parameterBody, multipartSchemaOf(program, parameterBody, registry)),
2593
+ textParts: textPartsOf(program, parameterBody),
2594
+ };
1543
2595
  }
1544
- return requestType === undefined ? undefined : registry.expressionFor(requestType);
2596
+ if (requestType === undefined)
2597
+ return { requestSchema: undefined };
2598
+ const declared = withMetadataAnnotationsIgnored(ignoresMetadata(parameterBody), () => registry.expressionFor(requestType, true));
2599
+ const form = formBodyOf(program, registry, operation, parameterBody, requestType, declared);
2600
+ return form === undefined
2601
+ ? { requestSchema: declared }
2602
+ : form.only
2603
+ ? { requestSchema: form.schema }
2604
+ : { requestSchema: declared, formRequestSchema: form.schema };
1545
2605
  }),
1546
2606
  ...(() => {
1547
- const split = withVisibility(program, requestVisibility, () => parameterSchemasOf(program, operation, registry, template.segments, template.literalQuery.map(([name]) => name)));
2607
+ const split = withVisibility(requestVisibility, () => parameterSchemasOf(program, operation, registry, template.segments, template.literalQuery.map(([name]) => name)));
1548
2608
  return {
1549
2609
  cookieSchema: split.cookie,
1550
2610
  pathSchema: split.path,
@@ -1561,7 +2621,7 @@ export function collectRoutes(program, registry, service) {
1561
2621
  * visibility declared six components under the wrong suffix - measured:
1562
2622
  * type/model/visibility gained six divergences.
1563
2623
  */
1564
- responses: withVisibility(program, Visibility.Read, () => responsesOf(program, operation, registry)),
2624
+ responses: withVisibility(Visibility.Read, () => responsesOf(program, operation, registry)),
1565
2625
  rawBodyProperty,
1566
2626
  rawBodyBinary,
1567
2627
  rawBodyUnvalidated,
@@ -1776,7 +2836,8 @@ function renderSchemas(registry, externals, routeDeclarations, compileSchemas) {
1776
2836
  if (d.structural === undefined) {
1777
2837
  return `\nexport const ${d.identifier} = ${compiled(d.source, compileSchemas)};\nexport type ${d.typeName} = z.infer<typeof ${d.identifier}>;\n`;
1778
2838
  }
1779
- const declared = d.structural.startsWith("{")
2839
+ // One object literal is an interface; a union of them - an envelope's wrappers - is not.
2840
+ const declared = isSingleObjectLiteral(d.structural)
1780
2841
  ? `export interface ${d.typeName} ${d.structural}`
1781
2842
  : `export type ${d.typeName} = ${d.structural};`;
1782
2843
  /**
@@ -1898,11 +2959,27 @@ function renderSchemas(registry, externals, routeDeclarations, compileSchemas) {
1898
2959
  * reason `z` is imported only when used: an unused declaration fails the lint a generated file has
1899
2960
  * to pass. Declared rather than imported, so this file still depends on no runtime.
1900
2961
  */
1901
- const referenced = identifiersIn(routeDeclarations.join(""));
2962
+ const referenced = identifiersIn([...declarations, ...routeDeclarations].join(""));
2963
+ // Both decoders read their values through `wireRead`, so it is declared beside either.
2964
+ const expands = referenced.has("uriExpansion") ||
2965
+ referenced.has("explodedQuery") ||
2966
+ referenced.has("wireReadObject");
2967
+ // And `wireReadAs` reads through `wireRead`, which a multipart text part calls on its own.
2968
+ const readsAs = expands || referenced.has("wireReadAs");
2969
+ if (readsAs || referenced.has("wireRead"))
2970
+ parts.push(WIRE_READ_SOURCE);
2971
+ if (readsAs)
2972
+ parts.push(WIRE_READ_AS_SOURCE);
2973
+ if (expands)
2974
+ parts.push(WIRE_READERS_SOURCE);
1902
2975
  if (referenced.has("uriExpansion"))
1903
2976
  parts.push(URI_EXPANSION_SOURCE);
1904
2977
  if (referenced.has("explodedQuery"))
1905
2978
  parts.push(EXPLODED_QUERY_SOURCE);
2979
+ if (referenced.has("wireReadObject"))
2980
+ parts.push(WIRE_READ_OBJECT_SOURCE);
2981
+ if (referenced.has("wireItself"))
2982
+ parts.push(WIRE_ITSELF_SOURCE);
1906
2983
  if (routeDeclarations.length > 0) {
1907
2984
  parts.push(`
1908
2985
  /** One declared response: its status key, its body's schema, its media types and its headers. */
@@ -1928,14 +3005,55 @@ type ResponseArm = {
1928
3005
  * keeps it, so the generated file does not fill up with aliases that alias nothing.
1929
3006
  */
1930
3007
  const isIdentifier = (expression) => /^[A-Za-z_$][\w$]*$/.test(expression);
1931
- const schemaConst = (operationId, suffix) => `${operationId}${suffix}`;
3008
+ /**
3009
+ * The identifier a route's validator is exported under: the operation's id and the part it checks.
3010
+ *
3011
+ * **Never one of the helpers the module declares beside it.** An operation named `exploded` has a
3012
+ * query validator called `explodedQuery`, which is also the name of the function that gathers an
3013
+ * exploded query object, and the module did not parse. Such a name takes a trailing `_`, as a model
3014
+ * named after an emitted helper does.
3015
+ */
3016
+ const schemaConst = (operationId, suffix, taken) => {
3017
+ /**
3018
+ * **And always an identifier, and never one already taken.** `@operationId("get-widget")` is a
3019
+ * valid id and not a valid name: `export const get-widgetQuery` did not parse. Each character no
3020
+ * identifier may hold becomes `_`, and a name another operation already took - `get_widget`
3021
+ * beside `get-widget` - takes one more.
3022
+ */
3023
+ let name = `${operationId.replace(/[^\w$]/g, "_").replace(/^(\d)/, "_$1")}${suffix}`;
3024
+ while (EMITTED_FUNCTIONS.has(name) || taken.has(name))
3025
+ name = `${name}_`;
3026
+ taken.add(name);
3027
+ return name;
3028
+ };
3029
+ /**
3030
+ * Text written inside a block comment, which `*\/` would end: `@operationId("list*\/items")` is a
3031
+ * valid id, and the comment naming it closed early and left the rest of the line as code.
3032
+ */
3033
+ function commentSafe(text) {
3034
+ // And printable ASCII, as every generated file is: a letter outside it is written as its escape.
3035
+ return text
3036
+ .replaceAll("*/", "*\\/")
3037
+ .replace(/[^\x20-\x7e]/gu, (each) => `\\u{${(each.codePointAt(0) ?? 0).toString(16)}}`);
3038
+ }
3039
+ /** The functions a schemas module may declare for itself - see `helperSources`. */
3040
+ const EMITTED_FUNCTIONS = new Set([
3041
+ "uriExpansion",
3042
+ "explodedQuery",
3043
+ "wireRead",
3044
+ "wireReadAs",
3045
+ "wireReaderOf",
3046
+ "wireReadObject",
3047
+ "wireItself",
3048
+ "wireSentAsItself",
3049
+ ]);
1932
3050
  /** `[the name to refer to it by, the `const` line to emit]` - the line is absent when none is needed. */
1933
- function namedSchema(operationId, suffix, expression, compileSchemas) {
3051
+ function namedSchema(operationId, suffix, expression, compileSchemas, taken) {
1934
3052
  if (expression === undefined)
1935
3053
  return { name: undefined, declaration: undefined };
1936
3054
  if (isIdentifier(expression))
1937
3055
  return { name: expression, declaration: undefined };
1938
- const name = schemaConst(operationId, suffix);
3056
+ const name = schemaConst(operationId, suffix, taken);
1939
3057
  /**
1940
3058
  * **Exported, because a schema nothing outside this file can name is a schema nothing can
1941
3059
  * check.** These are the validators the server actually enforces on every request - the path,
@@ -1992,10 +3110,11 @@ function sharedSlots(routes) {
1992
3110
  function nameRouteSchemas(routes, compileSchemas) {
1993
3111
  const shared = sharedSlots(routes);
1994
3112
  const declarations = [];
3113
+ const taken = new Set();
1995
3114
  const names = new Map();
1996
3115
  for (const route of routes) {
1997
3116
  const declare = (suffix, expression) => {
1998
- const resolved = namedSchema(route.operationId, suffix, expression, compileSchemas);
3117
+ const resolved = namedSchema(route.operationId, suffix, expression, compileSchemas, taken);
1999
3118
  if (resolved.declaration !== undefined)
2000
3119
  declarations.push(resolved.declaration);
2001
3120
  return resolved.name;
@@ -2016,6 +3135,9 @@ function nameRouteSchemas(routes, compileSchemas) {
2016
3135
  const header = declare("Header", negotiating ? route.negotiatedHeaderSchema : route.headerSchema);
2017
3136
  // A raw body is read as text, never parsed, so it has no validator - the bytes are the contract.
2018
3137
  const body = route.rawBodyProperty === undefined ? declare("Body", route.requestSchema) : undefined;
3138
+ const formBody = route.rawBodyProperty === undefined
3139
+ ? declare("FormBody", route.formRequestSchema)
3140
+ : undefined;
2019
3141
  /**
2020
3142
  * One identifier per distinct body, suffixed by the status that first carries it. Two statuses
2021
3143
  * sharing one inline body share its declaration rather than emitting it twice.
@@ -2045,7 +3167,7 @@ function nameRouteSchemas(routes, compileSchemas) {
2045
3167
  }
2046
3168
  return { status: response.status, schema: declaredBodies.get(response.schema), headers };
2047
3169
  });
2048
- const responses = schemaConst(route.operationId, "Responses");
3170
+ const responses = schemaConst(route.operationId, "Responses", taken);
2049
3171
  declarations.push(`export const ${responses} = ${responseArmsOf(route, arms)} satisfies readonly ResponseArm[];`);
2050
3172
  names.set(route.operationId, {
2051
3173
  operationId: route.operationId,
@@ -2056,6 +3178,7 @@ function nameRouteSchemas(routes, compileSchemas) {
2056
3178
  selectedAccept: negotiating ? route.accept : undefined,
2057
3179
  cookie,
2058
3180
  body,
3181
+ ...(formBody === undefined ? {} : { formBody }),
2059
3182
  arms,
2060
3183
  responses,
2061
3184
  });
@@ -2127,15 +3250,14 @@ function collectRequestTypes(program, registry, service) {
2127
3250
  * caller satisfying the published type sent read-only fields and a strict validator answered
2128
3251
  * 400.
2129
3252
  *
2130
- * It is also what keeps `metadataInfo` scoped: the predicates `propertyToTs` consults live
2131
- * behind `withVisibility`, and calling them outside one leaves a module-level singleton built
2132
- * for the wrong program.
3253
+ * The predicates `propertyToTs` consults read the visibility `withVisibility` sets, and outside
3254
+ * one they read `Read`.
2133
3255
  */
2134
- withVisibility(program, resolveRequestVisibility(program, operation.operation, operation.verb), () => {
3256
+ withVisibility(requestVisibilityOf(program, operation), () => {
2135
3257
  const body = requestBodyOf(operation);
2136
3258
  const placement = requestBodyPlacementOf(program, operation);
2137
3259
  /** The parts of a multipart body, which `requestBodyOf` deliberately does not return. */
2138
- const parameterBody = operation.parameters.body;
3260
+ const parameterBody = requestBodyParameterOf(operation);
2139
3261
  /**
2140
3262
  * Headers are part of the input too.
2141
3263
  *
@@ -2143,7 +3265,7 @@ function collectRequestTypes(program, registry, service) {
2143
3265
  * beside the payload, they are arguments to the verification. Leaving them out of the wire
2144
3266
  * type would describe an input the application cannot act on.
2145
3267
  */
2146
- const extraProperties = operation.parameters.parameters
3268
+ const extraProperties = publishedParametersOf(operation)
2147
3269
  .filter((parameter) => parameter.type === "path" ||
2148
3270
  parameter.type === "query" ||
2149
3271
  parameter.type === "header" ||
@@ -2155,11 +3277,31 @@ function collectRequestTypes(program, registry, service) {
2155
3277
  * `"x-thing"` here too: a consumer checking its handlers against these types otherwise
2156
3278
  * fails to typecheck against a server that is correct.
2157
3279
  */
2158
- .map((parameter) => registry.expressionForProperty(parameter.param, parameter.name));
3280
+ // One value per name and location, as the document publishes the first declaration.
3281
+ .filter((parameter, index, all) => all.findIndex((other) => sameWireName(other, parameter)) === index)
3282
+ .map((parameter) => ({
3283
+ key: parameter.name,
3284
+ // At the visibility its shared component is published at - see `parameterVisibilityOf`.
3285
+ // Refused as `never`, as its validator is - a header case collision, or a `@useRef`.
3286
+ line: headerCaseCollisionOf(program, operation, parameter) ||
3287
+ externalReferenceOf(parameter.param) !== undefined
3288
+ ? `${objectKey(parameter.name)}${parameter.param.optional ? "?" : ""}: never;`
3289
+ : withVisibility(parameterVisibilityOf(parameter.param) ?? currentVisibility(), () => registry.expressionForProperty(parameter.param, parameter.name)),
3290
+ }));
2159
3291
  const fileContentType = fileContentTypeOf(operation);
2160
3292
  if (fileContentType !== undefined) {
2161
- extraProperties.push(`${objectKey("Content-Type")}: ${registry.expressionFor(fileContentType.type)};`);
3293
+ extraProperties.push({
3294
+ key: "Content-Type",
3295
+ line: `${objectKey("Content-Type")}: ${registry.expressionFor(fileContentType.type)};`,
3296
+ });
2162
3297
  }
3298
+ /**
3299
+ * **A key two parameters share is written once per declaration, each in its own part**,
3300
+ * so the intersection types it as both - what the validators beside it accept - where two
3301
+ * members of one object literal do not compile (TS2300). `shared-input-key` reports it.
3302
+ */
3303
+ const repeated = extraProperties.filter((property, index) => extraProperties.slice(0, index).some((earlier) => earlier.key === property.key));
3304
+ const distinct = extraProperties.filter((property) => !repeated.includes(property));
2163
3305
  const parts = [];
2164
3306
  let streamsBody = false;
2165
3307
  let bodyRef;
@@ -2178,15 +3320,16 @@ function collectRequestTypes(program, registry, service) {
2178
3320
  * it, though the input is the text or the bytes that arrived. 0.27.0 declared it, and a
2179
3321
  * consumer importing it lost the import when the body became raw.
2180
3322
  */
2181
- if (body !== undefined && declaresComponent(body))
2182
- registry.expressionFor(body);
3323
+ if (body !== undefined && declaresComponent(body)) {
3324
+ withMetadataAnnotationsIgnored(ignoresMetadata(parameterBody), () => registry.expressionFor(body));
3325
+ }
2183
3326
  }
2184
3327
  else {
2185
3328
  const expression = parameterBody?.bodyKind === "multipart"
2186
- ? multipartTsOf({ parts: parameterBody.parts }, registry)
3329
+ ? multipartTsOf(program, parameterBody, registry)
2187
3330
  : body === undefined
2188
3331
  ? undefined
2189
- : registry.expressionFor(body);
3332
+ : withMetadataAnnotationsIgnored(ignoresMetadata(parameterBody), () => registry.expressionFor(body, true));
2190
3333
  if (parameterBody?.bodyKind !== "multipart")
2191
3334
  bodyRef = expression;
2192
3335
  if (expression !== undefined) {
@@ -2195,13 +3338,17 @@ function collectRequestTypes(program, registry, service) {
2195
3338
  : `{\n\t${objectKey(placement.bodyProperty)}${placement.optionalBody ? "?" : ""}: ${expression.replaceAll("\n", "\n\t")};\n}`);
2196
3339
  }
2197
3340
  }
2198
- if (extraProperties.length > 0) {
2199
- parts.push(`{\n${extraProperties.map((line) => `\t${line}`).join("\n")}\n}`);
3341
+ if (distinct.length > 0) {
3342
+ parts.push(`{\n${distinct.map(({ line }) => `\t${line}`).join("\n")}\n}`);
2200
3343
  }
3344
+ for (const { line } of repeated)
3345
+ parts.push(`{\n\t${line}\n}`);
2201
3346
  entries.push({
2202
3347
  operationId: operationIdOf(program, operation.operation),
2203
3348
  bodyRef,
2204
- input: parts.length === 0 ? "Record<string, never>" : `Simplify<${parts.join(" & ")}>`,
3349
+ input: parts.length === 0
3350
+ ? "globalThis.Record<string, never>"
3351
+ : `Simplify<${parts.join(" & ")}>`,
2205
3352
  streamsBody,
2206
3353
  });
2207
3354
  });
@@ -2218,7 +3365,7 @@ function collectResponseTypes(program, registry, service) {
2218
3365
  * components under the wrong suffix. Saying it here rather than relying on the default is what
2219
3366
  * makes that impossible from either direction.
2220
3367
  */
2221
- withVisibility(program, Visibility.Read, () => {
3368
+ withVisibility(Visibility.Read, () => {
2222
3369
  for (const operation of service.operations) {
2223
3370
  /**
2224
3371
  * **The same responses, and the same members, the server emitters render.** This walk used
@@ -2244,7 +3391,8 @@ function collectResponseTypes(program, registry, service) {
2244
3391
  if (member.unvalidated &&
2245
3392
  member.response.body !== undefined &&
2246
3393
  declaresComponent(member.response.body)) {
2247
- registry.expressionFor(member.response.body);
3394
+ const declared = member.response.body;
3395
+ withMetadataAnnotationsIgnored(member.response.ignoresMetadata, () => registry.expressionFor(declared));
2248
3396
  }
2249
3397
  switch (member.serving) {
2250
3398
  case "none":
@@ -2258,9 +3406,10 @@ function collectResponseTypes(program, registry, service) {
2258
3406
  case "unvalidated":
2259
3407
  return "string";
2260
3408
  default: {
2261
- if (member.response.body === undefined)
3409
+ const declared = member.response.body;
3410
+ if (declared === undefined)
2262
3411
  return undefined;
2263
- const ref = registry.expressionFor(member.response.body);
3412
+ const ref = withMetadataAnnotationsIgnored(member.response.ignoresMetadata, () => registry.expressionFor(declared, true));
2264
3413
  bodyRefs.push(ref);
2265
3414
  return `Produced<${ref}>`;
2266
3415
  }
@@ -2284,8 +3433,8 @@ function collectResponseTypes(program, registry, service) {
2284
3433
  const every = headers.every(({ property }) => property.optional);
2285
3434
  const rendered = headers
2286
3435
  .map(({ name, property }) => property.optional
2287
- ? `readonly ${objectKey(name)}?: ${propertyTypeToTs(program, property)} | undefined`
2288
- : `readonly ${objectKey(name)}: ${propertyTypeToTs(program, property)}`)
3436
+ ? `readonly ${objectKey(name)}?: ${headerTypeOf(program, property)} | undefined`
3437
+ : `readonly ${objectKey(name)}: ${headerTypeOf(program, property)}`)
2289
3438
  .join("; ");
2290
3439
  fields.push(`readonly headers${every ? "?" : ""}: { ${rendered} }`);
2291
3440
  }
@@ -2343,9 +3492,9 @@ const BYTE_STREAM = `
2343
3492
  */
2344
3493
  export interface ByteStream {
2345
3494
  getReader(): {
2346
- read(): Promise<{ readonly done: boolean; readonly value?: Uint8Array | undefined }>;
3495
+ read(): globalThis.Promise<{ readonly done: boolean; readonly value?: Uint8Array | undefined }>;
2347
3496
  releaseLock(): void;
2348
- cancel?(reason?: unknown): Promise<void>;
3497
+ cancel?(reason?: unknown): globalThis.Promise<void>;
2349
3498
  };
2350
3499
  }
2351
3500
  `;
@@ -2382,7 +3531,7 @@ function renderRequestTypes(entries, responses, registry) {
2382
3531
  : `\nexport type ${declaration.name} = ${declaration.source};\n`;
2383
3532
  });
2384
3533
  const responseLookup = responses
2385
- .map((entry) => `\t/** Every success body \`${entry.operationId}\` declares. */\n\t${entry.operationId}: ${entry.output};`)
3534
+ .map((entry) => `\t/** Every success body \`${commentSafe(entry.operationId)}\` declares. */\n\t${objectKey(entry.operationId)}: ${entry.output};`)
2386
3535
  .join("\n");
2387
3536
  /**
2388
3537
  * **One member per declared response and media type, each pairing a status with its own body.**
@@ -2390,7 +3539,7 @@ function renderRequestTypes(entries, responses, registry) {
2390
3539
  * over a service binding returns `WireResults[op]`, and a handler can hand it straight on.
2391
3540
  */
2392
3541
  const resultLookup = responses
2393
- .map((entry) => `\t/** Every response \`${entry.operationId}\` declares. */\n\t${entry.operationId}:${entry.result.length === 0 ? " never" : entry.result.map((member) => `\n\t\t| ${member}`).join("")};`)
3542
+ .map((entry) => `\t/** Every response \`${commentSafe(entry.operationId)}\` declares. */\n\t${objectKey(entry.operationId)}:${entry.result.length === 0 ? " never" : entry.result.map((member) => `\n\t\t| ${member}`).join("")};`)
2394
3543
  .join("\n");
2395
3544
  /**
2396
3545
  * **Keyed by operation id, and the only name an operation's input has.**
@@ -2403,7 +3552,7 @@ function renderRequestTypes(entries, responses, registry) {
2403
3552
  * `WireInputs["createUser"]` is the same type, and nothing a spec declares can collide with it.
2404
3553
  */
2405
3554
  const lookup = entries
2406
- .map((entry) => `\t/** The input for \`${entry.operationId}\`, as its handler receives it. */\n\t${entry.operationId}: ${entry.input.replaceAll("\n", "\n\t")};`)
3555
+ .map((entry) => `\t/** The input for \`${commentSafe(entry.operationId)}\`, as its handler receives it. */\n\t${objectKey(entry.operationId)}: ${entry.input.replaceAll("\n", "\n\t")};`)
2407
3556
  .join("\n");
2408
3557
  /**
2409
3558
  * **Emitted only where an operation has something to flatten.**
@@ -2448,8 +3597,9 @@ type Simplify<T> = { [K in keyof T]: T[K] } & {};
2448
3597
  * one service: 2 contract methods returning \`readonly T[]\` and 35 readonly array properties, none
2449
3598
  * of them expressible against the published shape.
2450
3599
  */
2451
- export type Produced<T> = T extends readonly (infer Element)[]
2452
- ? readonly Produced<Element>[]
3600
+ export type Produced<T> = T extends readonly unknown[]
3601
+ ? // Mapped over the list itself, so a tuple keeps its positions: \`[A?, ...unknown[]]\`.
3602
+ { readonly [K in keyof T]: Produced<T[K]> }
2453
3603
  : T extends object
2454
3604
  ? { readonly [K in keyof T]: Produced<T[K]> }
2455
3605
  : T;
@@ -2550,7 +3700,7 @@ function renderWireAssertions(schemas, types, entries, responses, contractsPacka
2550
3700
  }
2551
3701
  if (paired.length === 0)
2552
3702
  return undefined;
2553
- const assertions = paired.map((pair) => `export type Assert${pair.name} = MustHold<Identical<Declared<z.input<typeof ${pair.identifier}>>, Contracts.${pair.name}>>;`);
3703
+ const assertions = paired.map((pair) => `export type Assert${pair.name} = MustHold<Identical<Declared<z.input<typeof ${pair.identifier}>>, Flat<Contracts.${pair.name}>>>;`);
2554
3704
  return `${generatedBanner()}
2555
3705
  import type { z } from "zod";
2556
3706
  import type * as Contracts from "${contractsPackage}";
@@ -2612,10 +3762,9 @@ type DeclaredKeyOf<T> = keyof {
2612
3762
 
2613
3763
  type Declared<T> = T extends (...args: never[]) => unknown
2614
3764
  ? T
2615
- : T extends readonly (infer Element)[]
2616
- ? T extends Element[]
2617
- ? Declared<Element>[]
2618
- : readonly Declared<Element>[]
3765
+ : T extends readonly unknown[]
3766
+ ? // Mapped over the list itself: a tuple keeps its positions, and \`readonly\` is kept as it is.
3767
+ { [K in keyof T]: Declared<T[K]> }
2619
3768
  : T extends object
2620
3769
  ? [DeclaredKeyOf<T>] extends [never]
2621
3770
  ? [T[keyof T]] extends [never]
@@ -2642,6 +3791,24 @@ type Declared<T> = T extends (...args: never[]) => unknown
2642
3791
  }
2643
3792
  : T;
2644
3793
 
3794
+ /**
3795
+ * **The contract type with its intersections written out**, as \`Declared\` writes out the validator's:
3796
+ * \`(A) & (Inner)\` - a redeclaration merged with a \`@oneOf\` union it cannot be merged into key by key
3797
+ * - and the one object type a mapping makes of it are one shape, which the identity check below tells
3798
+ * apart by spelling. Mapped and nothing dropped, so an index signature the contract states is still
3799
+ * compared.
3800
+ */
3801
+ type Flat<T> = T extends (...args: never[]) => unknown
3802
+ ? T
3803
+ : T extends object
3804
+ ? [keyof T] extends [never]
3805
+ ? // **Every object with no key is the one \`{}\`**, as \`Declared\` writes an empty object: an
3806
+ // empty interface beside an empty literal, \`M1 | {}\`, is otherwise two members where
3807
+ // the validator's side is one.
3808
+ {}
3809
+ : { [K in keyof T]: Flat<T[K]> }
3810
+ : T;
3811
+
2645
3812
  /**
2646
3813
  * **The constraint is what fails the build.** \`Identical<A, B> & true\` merely produces an odd
2647
3814
  * type when the two disagree, and a type alias to an odd type compiles fine - 66 assertions that
@@ -2813,124 +3980,190 @@ export async function emitHttpZod(context) {
2813
3980
  for (const snapshot of snapshots) {
2814
3981
  beginOperationIds();
2815
3982
  const service = snapshot.service;
2816
- const serviceName = service.namespace.name;
2817
- const perService = options.services?.[serviceName];
2818
- const outputDir = perService?.["output-dir"] ??
2819
- (distinctServices.size > 1
2820
- ? resolvePath(context.emitterOutputDir, serviceName)
2821
- : context.emitterOutputDir);
2822
3983
  /**
2823
- * **Split where two services would write one file, and nowhere else.** A shared contracts
2824
- * directory was written once per service to one path, so the second service's
2825
- * `requests.gen.ts` replaced the first's and a whole surface lost its contracts with no
2826
- * diagnostic. Counted over the services that WRITE there: one service falling back to the
2827
- * shared directory, beside others that name their own, collides with nothing, and splitting
2828
- * it moved contracts a project already imported. A relative `contracts-package` is resolved
2829
- * from each service's own `wire-contract.gen.ts`, so it names that service's contracts; a
2830
- * package specifier has to be set per service.
3984
+ * **Rendered whole before anything is written.** This service's view, its operation ids and
3985
+ * its banner are module state, and an `await` while they are in force lets another compile in
3986
+ * the same process run in between: the contract types were walked after `schemas.gen.ts` was
3987
+ * written, and with three fixtures compiled at once in one suite, one walked them under
3988
+ * another program's view - its components unsuffixed, `DerivedK` declared twice, its wire
3989
+ * contract unable to pair a validator with its type. Written after the scope closes, nothing
3990
+ * awaits inside it.
2831
3991
  */
2832
- const sharedContractsDir = options["contracts-output-dir"];
2833
- const resolved = {
2834
- contractsOutputDir: perService?.["contracts-output-dir"] ??
2835
- (sharedContractsDir !== undefined && sharingContracts.length > 1
2836
- ? resolvePath(sharedContractsDir, serviceName)
2837
- : sharedContractsDir),
2838
- contractsPackage: perService?.["contracts-package"] ?? options["contracts-package"],
2839
- sealObjectSchemas: perService?.["seal-object-schemas"] ?? options["seal-object-schemas"] ?? false,
2840
- compileSchemas: perService?.["compile-schemas"] ?? options["compile-schemas"] ?? false,
2841
- keyVocabularies: perService?.["key-vocabularies"] ?? options["key-vocabularies"] ?? [],
2842
- regenerateHint: perService?.["regenerate-hint"] ?? options["regenerate-hint"],
2843
- };
2844
- beginBanner(resolved.regenerateHint);
2845
- const registry = new SchemaRegistry(context.program);
2846
- const { result: { routes }, imports: externals, vocabularies, } = collectExternalImports(() => withContractsPackage(resolved.contractsPackage, () => withSealedObjects(resolved.sealObjectSchemas, () => {
2847
- // Declarations first: a vocabulary is a fact about the contract, so it is collected from
2848
- // what the service DECLARES rather than from what the Zod walk happens to touch. An enum
2849
- // reachable only through an error body was silently absent otherwise.
2850
- noteDeclaredVocabularies(service.namespace.enums.values());
3992
+ const writes = [];
3993
+ // Named and written as openapi3 names and writes this service's components - see `published.ts`.
3994
+ const restoreView = beginServiceView(context.program, service.namespace);
3995
+ try {
2851
3996
  /**
2852
- * A closed key SET is a vocabulary too, and it cannot be an enum.
2853
- *
2854
- * TypeSpec has no key-constrained `Record`, so a model naming its permitted properties is
2855
- * how the document states one - and a generated type cannot be iterated, which leaves a
2856
- * consumer needing the set at run time with a hand-maintained copy. Naming the model in
2857
- * `key-vocabularies` writes the keys beside the types. A name matching nothing is
2858
- * reported rather than quietly emitting no tuple.
3997
+ * **A union that reaches itself through unions alone is refused before anything is walked,
3998
+ * and the service emits nothing.** Every other refusal is reported and walked past, so one
3999
+ * compile names them all; this one cannot be walked past, because every reading of it - the
4000
+ * schema, the type, a parameter's forms - recurses without end. The rest of the service is
4001
+ * read once the cycle is broken. See `reachesItselfThroughUnions`.
2859
4002
  */
2860
- const emittedKeyVocabularies = noteKeyVocabularies(context.program, service.namespace.models.values(), resolved.keyVocabularies);
2861
- for (const name of resolved.keyVocabularies) {
2862
- if (emittedKeyVocabularies.includes(name))
2863
- continue;
2864
- reportDiagnostic(context.program, {
2865
- code: "unknown-key-vocabulary",
2866
- target: service.namespace,
2867
- format: { name, service: service.namespace.name },
2868
- });
2869
- }
4003
+ const selfReaching = [...publishedTypes()].filter((type) => type.kind === "Union" &&
4004
+ type.name !== undefined &&
4005
+ reachesItselfThroughUnions(context.program, type));
4006
+ for (const union of selfReaching)
4007
+ refuseSelfReachingUnion(context.program, union);
4008
+ if (selfReaching.length > 0)
4009
+ continue;
4010
+ const serviceName = service.namespace.name;
4011
+ const perService = options.services?.[serviceName];
4012
+ const outputDir = perService?.["output-dir"] ??
4013
+ (distinctServices.size > 1
4014
+ ? resolvePath(context.emitterOutputDir, serviceName)
4015
+ : context.emitterOutputDir);
2870
4016
  /**
2871
- * The error body is registered INSIDE this walk, so its validator lands in
2872
- * `schemas.gen.ts` alongside every other declaration. Nothing else reaches it: the Zod
2873
- * walk follows success bodies and parameters, so without this a runtime would have no
2874
- * generated schema to check the documents it sends against.
2875
- *
2876
- * **Its expression is registered and then deliberately discarded.** It used to be
2877
- * returned and used as a fallback for any response arm whose own body could not be
2878
- * resolved, which meant every catch-all answered with whichever shape `errorBodyOf` had
2879
- * settled on rather than the one its own response declares. Each arm resolves its own
2880
- * body now; registering this one only ensures it is declared.
4017
+ * **Split where two services would write one file, and nowhere else.** A shared contracts
4018
+ * directory was written once per service to one path, so the second service's
4019
+ * `requests.gen.ts` replaced the first's and a whole surface lost its contracts with no
4020
+ * diagnostic. Counted over the services that WRITE there: one service falling back to the
4021
+ * shared directory, beside others that name their own, collides with nothing, and splitting
4022
+ * it moved contracts a project already imported. A relative `contracts-package` is resolved
4023
+ * from each service's own `wire-contract.gen.ts`, so it names that service's contracts; a
4024
+ * package specifier has to be set per service.
2881
4025
  */
2882
- const errorBody = errorBodyOf(context.program, service);
2883
- if (errorBody !== undefined) {
2884
- withVisibility(context.program, Visibility.Read, () => registry.expressionFor(errorBody));
2885
- }
2886
- return {
2887
- routes: withStreamModule(streamModule, () => collectRoutes(context.program, registry, service)),
4026
+ const sharedContractsDir = options["contracts-output-dir"];
4027
+ const resolved = {
4028
+ contractsOutputDir: perService?.["contracts-output-dir"] ??
4029
+ (sharedContractsDir !== undefined && sharingContracts.length > 1
4030
+ ? resolvePath(sharedContractsDir, serviceName)
4031
+ : sharedContractsDir),
4032
+ contractsPackage: perService?.["contracts-package"] ?? options["contracts-package"],
4033
+ sealObjectSchemas: perService?.["seal-object-schemas"] ?? options["seal-object-schemas"] ?? false,
4034
+ compileSchemas: perService?.["compile-schemas"] ?? options["compile-schemas"] ?? false,
4035
+ keyVocabularies: perService?.["key-vocabularies"] ?? options["key-vocabularies"] ?? [],
4036
+ regenerateHint: perService?.["regenerate-hint"] ?? options["regenerate-hint"],
2888
4037
  };
2889
- })));
2890
- /**
2891
- * **Named AFTER the walk and BEFORE the file is rendered.** Naming mints identifiers that
2892
- * `schemas.gen.ts` must declare, so it has to happen while the registry is complete and before
2893
- * anything is written - and it has to happen HERE rather than in a server emitter, or the
2894
- * validators it produces never reach this package's own output.
2895
- */
2896
- const { names, declarations } = nameRouteSchemas(routes, resolved.compileSchemas);
2897
- await emitFile(context.program, {
2898
- path: resolvePath(outputDir, "schemas.gen.ts"),
2899
- content: renderSchemas(registry, externals, declarations, resolved.compileSchemas),
2900
- });
2901
- if (resolved.contractsOutputDir !== undefined) {
2902
- const types = new TypeRegistry(context.program);
4038
+ beginBanner(resolved.regenerateHint);
4039
+ const registry = new SchemaRegistry(context.program);
4040
+ const { result: { routes }, imports: externals, vocabularies, } = collectExternalImports(() => withContractsPackage(resolved.contractsPackage, () => withSealedObjects(resolved.sealObjectSchemas, () => {
4041
+ // Declarations first: a vocabulary is a fact about the contract, so it is collected from
4042
+ // what the service DECLARES rather than from what the Zod walk happens to touch. An enum
4043
+ // reachable only through an error body was silently absent otherwise.
4044
+ noteDeclaredVocabularies(service.namespace.enums.values());
4045
+ /**
4046
+ * A closed key SET is a vocabulary too, and it cannot be an enum.
4047
+ *
4048
+ * TypeSpec has no key-constrained `Record`, so a model naming its permitted properties is
4049
+ * how the document states one - and a generated type cannot be iterated, which leaves a
4050
+ * consumer needing the set at run time with a hand-maintained copy. Naming the model in
4051
+ * `key-vocabularies` writes the keys beside the types. A name matching nothing is
4052
+ * reported rather than quietly emitting no tuple.
4053
+ */
4054
+ const emittedKeyVocabularies = noteKeyVocabularies(context.program, service.namespace.models.values(), resolved.keyVocabularies);
4055
+ for (const name of resolved.keyVocabularies) {
4056
+ if (emittedKeyVocabularies.includes(name))
4057
+ continue;
4058
+ reportDiagnostic(context.program, {
4059
+ code: "unknown-key-vocabulary",
4060
+ target: service.namespace,
4061
+ format: { name, service: service.namespace.name },
4062
+ });
4063
+ }
4064
+ /**
4065
+ * The error body is registered INSIDE this walk, so its validator lands in
4066
+ * `schemas.gen.ts` alongside every other declaration. Nothing else reaches it: the Zod
4067
+ * walk follows success bodies and parameters, so without this a runtime would have no
4068
+ * generated schema to check the documents it sends against.
4069
+ *
4070
+ * **Its expression is registered and then deliberately discarded.** It used to be
4071
+ * returned and used as a fallback for any response arm whose own body could not be
4072
+ * resolved, which meant every catch-all answered with whichever shape `errorBodyOf` had
4073
+ * settled on rather than the one its own response declares. Each arm resolves its own
4074
+ * body now; registering this one only ensures it is declared.
4075
+ */
4076
+ const errorBody = errorBodyOf(context.program, service);
4077
+ if (errorBody !== undefined) {
4078
+ withVisibility(Visibility.Read, () => registry.expressionFor(errorBody));
4079
+ }
4080
+ const routes = withStreamModule(streamModule, () => collectRoutes(context.program, registry, service));
4081
+ // And every type no operation reaches, as openapi3 publishes it - see `unreachedTypes`.
4082
+ withVisibility(Visibility.All, () => {
4083
+ for (const type of unreachedTypes())
4084
+ registry.expressionFor(type);
4085
+ });
4086
+ // And every component openapi3 writes that no route names - see `publishedComponents`.
4087
+ for (const { type, at, ignore, binary } of publishedComponents(context.program)) {
4088
+ withVisibility(at, () => withMetadataAnnotationsIgnored(ignore, () => withMultipartBinary(binary, () => registry.declare(type))));
4089
+ }
4090
+ return { routes };
4091
+ })));
2903
4092
  /**
2904
- * **Inside the same stream scope as the route walk**, because both read the same response
2905
- * records: outside it `streamedTypeOf` cannot see `@typespec/streams`, so an SSE body was
2906
- * stated as the string the document describes rather than the stream a server sends.
4093
+ * **Named AFTER the walk and BEFORE the file is rendered.** Naming mints identifiers that
4094
+ * `schemas.gen.ts` must declare, so it has to happen while the registry is complete and before
4095
+ * anything is written - and it has to happen HERE rather than in a server emitter, or the
4096
+ * validators it produces never reach this package's own output.
2907
4097
  */
2908
- const { entries, responses } = withStreamModule(streamModule, () => ({
2909
- entries: collectRequestTypes(context.program, types, service),
2910
- responses: collectResponseTypes(context.program, types, service),
2911
- }));
2912
- await emitFile(context.program, {
2913
- path: resolvePath(resolved.contractsOutputDir, "requests.gen.ts"),
2914
- content: renderRequestTypes(entries, responses, types),
4098
+ const { names, declarations } = nameRouteSchemas(routes, resolved.compileSchemas);
4099
+ writes.push({
4100
+ path: resolvePath(outputDir, "schemas.gen.ts"),
4101
+ content: renderSchemas(registry, externals, declarations, resolved.compileSchemas),
2915
4102
  });
2916
- await emitFile(context.program, {
2917
- path: resolvePath(resolved.contractsOutputDir, "vocabularies.gen.ts"),
2918
- content: renderVocabularies(vocabularies),
2919
- });
2920
- const assertions = renderWireAssertions(registry, types, entries, responses, resolved.contractsPackage);
2921
- if (assertions !== undefined) {
2922
- await emitFile(context.program, {
2923
- path: resolvePath(outputDir, "wire-contract.gen.ts"),
2924
- content: assertions,
4103
+ if (resolved.contractsOutputDir !== undefined) {
4104
+ const types = new TypeRegistry(context.program, registry.held);
4105
+ /**
4106
+ * **Inside the same stream scope as the route walk**, because both read the same response
4107
+ * records: outside it `streamedTypeOf` cannot see `@typespec/streams`, so an SSE body was
4108
+ * stated as the string the document describes rather than the stream a server sends.
4109
+ */
4110
+ /**
4111
+ * **And inside the same sealing**, because whether a model is published at all can depend
4112
+ * on it - a template instance extending a base openapi3 seals is refused on both sides,
4113
+ * `sealed-base-extended` - and the two walks have to agree about that.
4114
+ */
4115
+ const { requests, assertions } = withSealedObjects(resolved.sealObjectSchemas, () => {
4116
+ const { entries, responses } = withStreamModule(streamModule, () => ({
4117
+ entries: collectRequestTypes(context.program, types, service),
4118
+ responses: collectResponseTypes(context.program, types, service),
4119
+ }));
4120
+ withVisibility(Visibility.All, () => {
4121
+ for (const type of unreachedTypes())
4122
+ types.expressionFor(type);
4123
+ });
4124
+ for (const { type, at, ignore, binary } of publishedComponents(context.program)) {
4125
+ withVisibility(at, () => withMetadataAnnotationsIgnored(ignore, () => withMultipartBinary(binary, () => types.declare(type))));
4126
+ }
4127
+ types.refuse(registry.refusedNames());
4128
+ return {
4129
+ requests: renderRequestTypes(entries, responses, types),
4130
+ assertions: renderWireAssertions(registry, types, entries, responses, resolved.contractsPackage),
4131
+ };
2925
4132
  });
4133
+ writes.push({
4134
+ path: resolvePath(resolved.contractsOutputDir, "requests.gen.ts"),
4135
+ content: requests,
4136
+ });
4137
+ writes.push({
4138
+ path: resolvePath(resolved.contractsOutputDir, "vocabularies.gen.ts"),
4139
+ content: renderVocabularies(vocabularies),
4140
+ });
4141
+ if (assertions !== undefined) {
4142
+ writes.push({
4143
+ path: resolvePath(outputDir, "wire-contract.gen.ts"),
4144
+ content: assertions,
4145
+ });
4146
+ }
2926
4147
  }
4148
+ // Still inside this service's id scope, so each operation resolves to the id its route carries.
4149
+ const operations = new Map(service.operations.map((operation) => [
4150
+ operationIdOf(context.program, operation.operation),
4151
+ operation,
4152
+ ]));
4153
+ emitted.push({
4154
+ service,
4155
+ routes,
4156
+ schemaNames: names,
4157
+ operations,
4158
+ outputDir,
4159
+ options: resolved,
4160
+ });
4161
+ }
4162
+ finally {
4163
+ restoreView();
2927
4164
  }
2928
- // Still inside this service's id scope, so each operation resolves to the id its route carries.
2929
- const operations = new Map(service.operations.map((operation) => [
2930
- operationIdOf(context.program, operation.operation),
2931
- operation,
2932
- ]));
2933
- emitted.push({ service, routes, schemaNames: names, operations, outputDir, options: resolved });
4165
+ for (const write of writes)
4166
+ await emitFile(context.program, write);
2934
4167
  }
2935
4168
  return emitted;
2936
4169
  }