typespec-http-zod 0.1.0 → 0.3.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.
@@ -1,36 +1,11 @@
1
1
  import { getVisibilitySuffix, Visibility } from "@typespec/http";
2
- import { reportDiagnostic } from "./lib.js";
3
2
  import { propertyToTs, typeToTsBody, withTsRefResolver } from "./types.js";
4
3
  import { captureBackEdges, currentVisibility, isTransformedBy, noteBackEdge, propertyToZod, typeToZodBody, withRefResolver, withSyntheticDeclarations, } from "./zod.js";
5
- /**
6
- * Assigns each named TypeSpec model/enum a single Zod declaration, emitted in dependency order.
7
- *
8
- * Without this, a model used by twelve operations is inlined twelve times: the generated file grows
9
- * quadratically, and — worse — the *type* it infers is a fresh structural type at each site, so
10
- * `apps/web` gets twelve `Company`s that are only accidentally the same. One declaration per model
11
- * is what makes the emitted types usable as a shared vocabulary rather than as twelve coincidences.
12
- */
13
- /**
14
- * A cycle that reaches a declaration through nothing that can defer it.
15
- *
16
- * ⚠️ **A recursive model is NOT this.** `model InnerModel { children?: InnerModel[] }` is an ordinary
17
- * tree — `@typespec/openapi3` publishes a self-`$ref` for it and the document is valid — so it is
18
- * emitted as a reference, made lazy by a getter on the property that closes the loop (see
19
- * `modelToZod`). This placeholder is only for a cycle with no property on its path: a named **union**
20
- * whose variant refers back to a model that is still being rendered has nowhere to hang a getter, so
21
- * the reference cannot be deferred and the declaration would read itself while initialising.
22
- *
23
- * Reported rather than thrown, so the walk names every such cycle instead of dying inside the first.
24
- */
25
- const CYCLE_PLACEHOLDER_SCHEMA = "z.never()";
26
- function reportCycle(program, type, name) {
27
- reportDiagnostic(program, { code: "circular-model", target: type, format: { name } });
28
- }
29
4
  /**
30
5
  * The name an ANONYMOUS payload model inherits from the single model it spreads.
31
6
  *
32
- * ⚠️ **`@typespec/http` hands back a synthesised model for a body whose declaration carried
33
- * metadata**, and it has no name — so `@error model InvalidAuth { @statusCode _: 403; error: string }`
7
+ * **`@typespec/http` hands back a synthesised model for a body whose declaration carried
8
+ * metadata**, and it has no name - so `@error model InvalidAuth { @statusCode _: 403; error: string }`
34
9
  * reached the registry as `{error}` with `name: ""`, was inlined at its use site, and produced **no
35
10
  * validator at all** for a component the document publishes as `InvalidAuth`. Four authentication
36
11
  * scenarios, each with an unvalidated 403.
@@ -39,8 +14,8 @@ function reportCycle(program, type, name) {
39
14
  * itself, which is why its component keeps the original name. This recovers the same name from
40
15
  * `sourceModels`, so both artefacts call the shape the same thing.
41
16
  *
42
- * ⚠️ **Only a PURE spread of one named model.** `{...A, extra: string}` is a different shape and must
43
- * not claim `A`'s name — so every property has to come from the source. Metadata-stripping removes
17
+ * **Only a PURE spread of one named model.** `{...A, extra: string}` is a different shape and must
18
+ * not claim `A`'s name - so every property has to come from the source. Metadata-stripping removes
44
19
  * properties and never adds them, which is exactly the asymmetry this tests for. A collision would be
45
20
  * caught by the differential's `component-name-collision` arm regardless.
46
21
  */
@@ -48,11 +23,11 @@ function spreadSourceOf(type, seen = new Set(),
48
23
  /**
49
24
  * The properties the ORIGINAL type has, carried down the chain rather than re-derived.
50
25
  *
51
- * ⚠️ **Re-deriving them at each link is what blocked `Alias.InnerModel`.** The intermediate model
26
+ * **Re-deriving them at each link is what blocked `Alias.InnerModel`.** The intermediate model
52
27
  * an operation builds holds the spread body's properties AND the operation's own `@path` and
53
28
  * `@header` parameters, so checking THAT set against the named ancestor fails on properties the
54
29
  * body never had. The question is whether every property of the shape being named comes from the
55
- * ancestor — `{name}` does — not whether some intermediate did.
30
+ * ancestor - `{name}` does - not whether some intermediate did.
56
31
  *
57
32
  * The guard is unchanged in what it refuses: `{...A, extra}` still fails, because `extra` is in
58
33
  * the original set and not in `A`.
@@ -74,9 +49,9 @@ required = [...type.properties.keys()]) {
74
49
  return undefined;
75
50
  }
76
51
  /**
77
- * ⚠️ **Follow the chain through ANONYMOUS links.** TypeSpec puts an unnamed model between an
52
+ * **Follow the chain through ANONYMOUS links.** TypeSpec puts an unnamed model between an
78
53
  * operation's parameter model and the model that was spread into it, so stopping at the first
79
- * source finds `""` and gives up — measured: the bodies for `Alias.InnerModel` and
54
+ * source finds `""` and gives up - measured: the bodies for `Alias.InnerModel` and
80
55
  * `Model.CompositeRequestMix` both reported `sources: [{ usage: "spread", name: "" }]` while the
81
56
  * document published both as components, and the one case that already worked
82
57
  * (`Model.BodyParameter`) was the one whose source happened to be named directly.
@@ -88,14 +63,14 @@ required = [...type.properties.keys()]) {
88
63
  return model.name === "" ? spreadSourceOf(model, new Set([...seen, type]), required) : model;
89
64
  }
90
65
  const spreadSourceName = (type) => spreadSourceOf(type)?.name;
91
- /** A type earns a name when the spec gave it one — anonymous shapes stay inline where they are used. */
66
+ /** A type earns a name when the spec gave it one - anonymous shapes stay inline where they are used. */
92
67
  function declaredNameOf(type) {
93
68
  if (type.kind === "Enum")
94
69
  return type.name;
95
70
  /**
96
71
  * A named union earns a declaration too.
97
72
  *
98
- * Without this a union request body is inlined at its use site, so it exports no type — and
73
+ * Without this a union request body is inlined at its use site, so it exports no type - and
99
74
  * `backend/contract.ts` and the wire assertions both pair a model with a schema **by name**. A
100
75
  * cross-field rule modelled as a discriminated union (which is how `@refine` is being replaced)
101
76
  * would silently stop being checked at the boundary.
@@ -110,10 +85,10 @@ function declaredNameOf(type) {
110
85
  if (type.name === "")
111
86
  return spreadSourceName(type);
112
87
  /**
113
- * ⚠️ **A template INSTANTIATION carries the template's name, and it is not a declaration.**
88
+ * **A template INSTANTIATION carries the template's name, and it is not a declaration.**
114
89
  *
115
90
  * `PublicSuccess<PublicCompanyDetail>` and `PublicSuccess<PublicOfficers>` are both called
116
- * `PublicSuccess`, so naming them would export eleven consts called `publicSuccessSchema` — the
91
+ * `PublicSuccess`, so naming them would export eleven consts called `publicSuccessSchema` - the
117
92
  * file does not compile, and if it did, one shape would validate every response. `@typespec/openapi3`
118
93
  * inlines instantiations for the same reason (the public document has no `PublicSuccess*`
119
94
  * component), so both artefacts agree about what has a name and what does not.
@@ -128,11 +103,11 @@ const lowerFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
128
103
  /**
129
104
  * One model can need MORE THAN ONE declaration.
130
105
  *
131
- * ⚠️ **Keyed on `(type, visibility)`, because a model is not one shape.** `@visibility` and HTTP
106
+ * **Keyed on `(type, visibility)`, because a model is not one shape.** `@visibility` and HTTP
132
107
  * metadata both mean the same declaration projects differently by position: `@typespec/openapi3`
133
108
  * emits `VisibilityModel` and `VisibilityModelCreate` as separate components for exactly this
134
109
  * reason. Keyed on the type alone, whichever position was walked first won and every other position
135
- * silently got its shape — measured on the corpus, one strict model carrying all six lifecycle
110
+ * silently got its shape - measured on the corpus, one strict model carrying all six lifecycle
136
111
  * properties, so a create request was required to send read-only fields.
137
112
  *
138
113
  * The suffix rule is openapi3's: canonical (`Read`) keeps the bare name, and another visibility only
@@ -141,10 +116,10 @@ const lowerFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
141
116
  */
142
117
  function keyFor(type, visibility) {
143
118
  /**
144
- * ⚠️ **Keyed on the SPREAD SOURCE where there is one, or one component gets two declarations.**
119
+ * **Keyed on the SPREAD SOURCE where there is one, or one component gets two declarations.**
145
120
  *
146
121
  * `@typespec/http` synthesises a fresh anonymous model per position, so two operations taking the
147
- * same body produce two distinct types that both resolve to the name `BodyModel` — and
122
+ * same body produce two distinct types that both resolve to the name `BodyModel` - and
148
123
  * `parameters/body-optionality` emitted `export const bodyModelSchema` twice, a file that does not
149
124
  * parse. The document has exactly one `BodyModel` component; collapsing onto the source is what
150
125
  * makes the two artefacts agree about how many things there are.
@@ -173,13 +148,15 @@ export class SchemaRegistry {
173
148
  #program;
174
149
  #declarations = new Map();
175
150
  #order = [];
176
- /** Key → the identifier it is *going* to bind, so a back edge can name a declaration in flight. */
151
+ /** Key -> the identifier it is *going* to bind, so a back edge can name a declaration in flight. */
177
152
  #inProgress = new Map();
153
+ /** Keys on a cycle - every one needs a structural type rather than a `z.infer` alias. */
154
+ #cyclic = new Set();
178
155
  constructor(program) {
179
156
  this.#program = program;
180
157
  }
181
158
  /**
182
- * The Zod expression for a type — a bare identifier once the type has a declaration, otherwise
159
+ * The Zod expression for a type - a bare identifier once the type has a declaration, otherwise
183
160
  * inline source. Declaring is depth-first so a model's dependencies are emitted above it, which is
184
161
  * what lets the generated file be plain `const` declarations with no forward references.
185
162
  */
@@ -188,7 +165,7 @@ export class SchemaRegistry {
188
165
  if (bare === undefined)
189
166
  return this.#inline(type);
190
167
  /**
191
- * ⚠️ **A type not reshaped at this visibility IS the canonical declaration — not a copy of it.**
168
+ * **A type not reshaped at this visibility IS the canonical declaration - not a copy of it.**
192
169
  *
193
170
  * Keying on the requested visibility while only suffixing transformed types produced two cache
194
171
  * entries sharing one identifier, and the emitted file stopped parsing:
@@ -207,15 +184,24 @@ export class SchemaRegistry {
207
184
  if (existing !== undefined)
208
185
  return existing.identifier;
209
186
  /**
210
- * ⚠️ **A declaration already in flight is REFERENCED, not re-entered.**
187
+ * **A declaration already in flight is REFERENCED, not re-entered.**
211
188
  *
212
189
  * Returning the identifier is what turns a recursive model into the self-`$ref` the document
213
- * publishes. The back edge is noted so the enclosing object knows to defer it behind a getter —
190
+ * publishes. The back edge is noted so the enclosing object knows to defer it behind a getter -
214
191
  * and if nothing does, {@link captureBackEdges} below still sees it and the cycle is named.
215
192
  */
216
193
  const pending = this.#inProgress.get(key);
217
194
  if (pending !== undefined) {
218
195
  noteBackEdge();
196
+ /**
197
+ * **Every declaration currently in flight is ON this cycle**, and all of them need a
198
+ * structural type - not only the one that ends up deferred. `#inProgress` is exactly the path
199
+ * from the cycle's entry point down to here, so recording it wholesale is both correct and
200
+ * cheap. Recording only the deferred declaration leaves the others as `z.infer` aliases and
201
+ * closes the loop again at the type level - `TS2456`, measured.
202
+ */
203
+ for (const inFlight of this.#inProgress.keys())
204
+ this.#cyclic.add(inFlight);
219
205
  return pending;
220
206
  }
221
207
  const name = `${bare}${at === Visibility.Read ? "" : getVisibilitySuffix(at, Visibility.Read)}`;
@@ -225,16 +211,17 @@ export class SchemaRegistry {
225
211
  this.#inProgress.delete(key);
226
212
  /**
227
213
  * A back edge that survived the whole body reached this declaration by a path with no object
228
- * property on it — a named union variant, or a dictionary value — so nothing could make it
229
- * lazy and the emitted `const` would read itself while initialising.
214
+ * property on it - a named union variant, or a dictionary value - so nothing could make it lazy
215
+ * and the emitted `const` would read itself while initialising. `z.lazy()` defers the whole body
216
+ * instead, which is the only place left to put the deferral once no property can hold it.
230
217
  */
231
- if (rendered.deferred)
232
- reportCycle(this.#program, type, bare);
233
- const source = rendered.deferred ? CYCLE_PLACEHOLDER_SCHEMA : rendered.value;
218
+ const source = rendered.deferred ? `z.lazy(() => ${rendered.value})` : rendered.value;
234
219
  const declaration = {
235
220
  identifier,
236
221
  typeName: name,
237
222
  source,
223
+ ...(this.#cyclic.has(key) ? { structural: this.#structuralBodyOf(type) } : {}),
224
+ ...(rendered.deferred ? { annotated: true } : {}),
238
225
  };
239
226
  this.#declarations.set(key, declaration);
240
227
  this.#order.push(declaration);
@@ -244,18 +231,18 @@ export class SchemaRegistry {
244
231
  /**
245
232
  * Declare the BASE CHAIN a component's own declaration implies.
246
233
  *
247
- * ⚠️ **Reachability from an operation is not the same set the document publishes.** openapi3 emits
234
+ * **Reachability from an operation is not the same set the document publishes.** openapi3 emits
248
235
  * a component for every base in an `allOf` chain whether or not an operation names it, so `Pet` and
249
- * `Cat` — bases of a `Siamese` that IS named — had no validator at all while the document declared
236
+ * `Cat` - bases of a `Siamese` that IS named - had no validator at all while the document declared
250
237
  * them.
251
238
  *
252
- * ⚠️ **Subtypes are deliberately NOT walked here, because nothing would be walking them.** A
239
+ * **Subtypes are deliberately NOT walked here, because nothing would be walking them.** A
253
240
  * discriminated base renders as `z.discriminatedUnion` over its subtypes, and rendering that
254
241
  * already asks the registry for each one. A loop here looked load-bearing and was measured not to
255
242
  * be: removing it left all 23 polymorphism and differential arms green.
256
243
  *
257
- * ⚠️ **Wrapped in {@link captureBackEdges} and the result discarded.** This call can land on a
258
- * declaration still in flight — a subtype's base is, by construction, the model that asked for it —
244
+ * **Wrapped in {@link captureBackEdges} and the result discarded.** This call can land on a
245
+ * declaration still in flight - a subtype's base is, by construction, the model that asked for it -
259
246
  * and that is not a reference in anyone's emitted expression. Letting the back edge escape would
260
247
  * have the enclosing declaration report a cycle it does not have and emit `z.never()`.
261
248
  */
@@ -268,7 +255,7 @@ export class SchemaRegistry {
268
255
  /**
269
256
  * The Zod for a single property, constraints and optionality included.
270
257
  *
271
- * Used for path/query parameters, which are `ModelProperty`s that belong to no model — so they
258
+ * Used for path/query parameters, which are `ModelProperty`s that belong to no model - so they
272
259
  * need the property-level walk rather than the type-level one, or `@minLength` on a path param
273
260
  * would be documented and never enforced.
274
261
  */
@@ -282,14 +269,14 @@ export class SchemaRegistry {
282
269
  /**
283
270
  * A declaration for a shape the spec never wrote, under a name the document publishes.
284
271
  *
285
- * `@discriminated` with an envelope has no TypeSpec type for its wrapper — openapi3 synthesises
272
+ * `@discriminated` with an envelope has no TypeSpec type for its wrapper - openapi3 synthesises
286
273
  * one and publishes it as `PetWithEnvelopeCat`, so there is a component with no `Type` to key on.
287
274
  * Keyed by name instead, and pushed in the same dependency-first order as everything else, so the
288
275
  * envelope declarations land above the union that references them.
289
276
  *
290
277
  * `declareNamed` exposes the same seam to the route emitter, which needs it for a **multipart**
291
278
  * body: the document publishes `MultiPartRequest` as a component, and there is no walkable type
292
- * for it — the parts have to be assembled from `@typespec/http`'s own view of the payload.
279
+ * for it - the parts have to be assembled from `@typespec/http`'s own view of the payload.
293
280
  */
294
281
  declareNamed(name, build) {
295
282
  return this.#declareSynthetic(name, build);
@@ -310,11 +297,28 @@ export class SchemaRegistry {
310
297
  this.#order.push(declaration);
311
298
  return identifier;
312
299
  }
300
+ /**
301
+ * The structural TypeScript body for a type on a cycle.
302
+ *
303
+ * **The same walk `requests.gen.ts` uses, resolving named types to their NAMES rather than
304
+ * declaring them.** A cyclic declaration cannot take its type from `z.infer<typeof ...>` - that is
305
+ * the loop - so it needs a type written out. Every name this body references is declared in the
306
+ * same file either structurally (if it is on the cycle too) or as the ordinary `z.infer` alias (if
307
+ * it is not), so nothing further has to be emitted for it.
308
+ *
309
+ * Sharing `types.ts` rather than writing a second TypeScript renderer is what keeps the annotation
310
+ * agreeing with the validator: those two walks are already paired against each other by
311
+ * `wire-contract.gen.ts`, and a third spelling of "what this shape is" would be a third thing to
312
+ * drift.
313
+ */
314
+ #structuralBodyOf(type) {
315
+ return withTsRefResolver((candidate) => declaredNameOf(candidate), () => typeToTsBody(this.#program, type));
316
+ }
313
317
  /**
314
318
  * Render `type` without consulting its own declaration, but letting nested types use theirs.
315
319
  *
316
- * ⚠️ **The "not itself" rule belongs to the TOP of the walk, not to every depth.** It used to live
317
- * in the resolver, which is asked at every node — so a model reaching itself through a property
320
+ * **The "not itself" rule belongs to the TOP of the walk, not to every depth.** It used to live
321
+ * in the resolver, which is asked at every node - so a model reaching itself through a property
318
322
  * was inlined again, and again: `type/array` and `type/dictionary` in the conformance corpus both
319
323
  * declare `children?: InnerModel[]` and both died with
320
324
  * `RangeError: Maximum call stack size exceeded`. Skipping the resolver once, here, is what makes
@@ -336,7 +340,7 @@ export class SchemaRegistry {
336
340
  * The same walk as {@link SchemaRegistry}, emitting plain TypeScript instead of Zod.
337
341
  *
338
342
  * Kept as a separate class rather than a mode on the schema registry because the two produce
339
- * different *kinds* of declaration — an `interface` versus a `const` — and their reference resolvers
343
+ * different *kinds* of declaration - an `interface` versus a `const` - and their reference resolvers
340
344
  * hand back different text. Sharing {@link declaredNameOf} is the part that matters: a model named in
341
345
  * one artefact is named the same way in the other, so the emitted assertion can pair them by name.
342
346
  */
@@ -356,9 +360,9 @@ export class TypeRegistry {
356
360
  if (existing !== undefined)
357
361
  return existing.name;
358
362
  /**
359
- * ⚠️ **No laziness is needed on this side, and no refusal either.** A TypeScript `interface`
360
- * may refer to itself — `interface InnerModel { children?: InnerModel[] }` is the ordinary way
361
- * to write a tree — so the name alone is the whole answer. The Zod registry has to defer the
363
+ * **No laziness is needed on this side, and no refusal either.** A TypeScript `interface`
364
+ * may refer to itself - `interface InnerModel { children?: InnerModel[] }` is the ordinary way
365
+ * to write a tree - so the name alone is the whole answer. The Zod registry has to defer the
362
366
  * same edge behind a getter only because a `const` cannot read itself while initialising.
363
367
  */
364
368
  if (this.#inProgress.has(type))
@@ -370,21 +374,21 @@ export class TypeRegistry {
370
374
  name,
371
375
  /**
372
376
  * Only a plain object literal can be an `interface`. A permissive response is
373
- * `{ …pinned } & Record<string, unknown>`, and `export interface X { … } & …` does not parse —
377
+ * `{ ...pinned } & Record<string, unknown>`, and `export interface X { ... } & ...` does not parse -
374
378
  * so an intersection is emitted as a type alias instead.
375
379
  */
376
380
  isObject: source.startsWith("{") && !source.includes("} & "),
377
381
  /**
378
- * ⚠️ **A vocabulary alias is NOT exported.**
382
+ * **A vocabulary alias is NOT exported.**
379
383
  *
380
384
  * It restates a vocabulary enum's members from the spec, and the vocabularies artefact already
381
- * exports the same 27 names derived from the runtime tuples — exporting both collides on
385
+ * exports the same 27 names derived from the runtime tuples - exporting both collides on
382
386
  * every one of them the moment the package re-exports this file.
383
387
  *
384
388
  * Restating rather than referencing is deliberate, and it is what makes the drift visible:
385
389
  * the emitted **Zod** defers to the runtime tuple while this type says what the spec
386
390
  * *declares*, so the two disagreeing is exactly the lie the document is telling. Referencing
387
- * the contracts type instead would make every assertion pass and publish the lie unchecked —
391
+ * the contracts type instead would make every assertion pass and publish the lie unchecked -
388
392
  * which is how `FilingState` came to advertise a `failed` state that cannot occur while
389
393
  * omitting the generated one a consumer actually uses.
390
394
  */
@@ -396,16 +400,17 @@ export class TypeRegistry {
396
400
  return declaration.name;
397
401
  }
398
402
  /**
399
- * The Zod-free type for a single property — used for path and query parameters, which are
403
+ * The Zod-free type for a single property - used for path and query parameters, which are
400
404
  * `ModelProperty`s belonging to no model. Without routing them through the registry a named model
401
405
  * or enum in a parameter position is inlined rather than referenced.
402
406
  */
403
- expressionForProperty(property) {
407
+ /** `wireName` names an HTTP parameter as the wire does - see {@link propertyToTs}. */
408
+ expressionForProperty(property, wireName) {
404
409
  return withTsRefResolver((candidate) => {
405
410
  if (declaredNameOf(candidate) === undefined)
406
411
  return undefined;
407
412
  return this.expressionFor(candidate);
408
- }, () => propertyToTs(this.#program, property));
413
+ }, () => propertyToTs(this.#program, property, wireName));
409
414
  }
410
415
  #inline(type) {
411
416
  return withTsRefResolver((candidate) => {
@@ -1,10 +1,10 @@
1
1
  import type { ZodType } from "zod";
2
2
  /**
3
- * **What generated code imports at run time — and the only thing this package puts in its output.**
3
+ * **What generated code imports at run time - and the only thing this package puts in its output.**
4
4
  *
5
- * ⚠️ **Kept deliberately tiny, and deliberately free of every TypeSpec import.** The main entry point
5
+ * **Kept deliberately tiny, and deliberately free of every TypeSpec import.** The main entry point
6
6
  * is an emitter: it pulls in `@typespec/compiler` and exists at build time. This module is what an
7
- * *application* touches — an Express or Fastify service reading the arms this emitter wrote — and
7
+ * *application* touches - an Express or Fastify service reading the arms this emitter wrote - and
8
8
  * making it reach the compiler would put a build-time dependency into a server's runtime graph for
9
9
  * the sake of two declarations. `test/packaging.test.ts` asserts the separation rather than trusting
10
10
  * it.
@@ -17,41 +17,41 @@ import type { ZodType } from "zod";
17
17
  * One status an operation may answer with, and the schema the document publishes for it.
18
18
  *
19
19
  * `when` is present only where an operation declares two success arms discriminated by a property of
20
- * the result — the application reads that property, because only it knows how to see inside its own
20
+ * the result - the application reads that property, because only it knows how to see inside its own
21
21
  * result envelope. Everything else about the choice is decided at generation time.
22
22
  *
23
- * ⚠️ **Failure arms are here too, and for a while they were not.** The document declares what an
24
- * error response looks like, and the generated server passed on only the success statuses — so an
23
+ * **Failure arms are here too, and for a while they were not.** The document declares what an
24
+ * error response looks like, and the generated server passed on only the success statuses - so an
25
25
  * application could not check what it sends when something goes wrong, because the schema never
26
26
  * arrived. Measured on one service: `401` and two `default` responses declared, zero emitted.
27
27
  *
28
- * ⚠️ **The arms arrive in OpenAPI's precedence order, so the FIRST match is the right one.** An exact
28
+ * **The arms arrive in OpenAPI's precedence order, so the FIRST match is the right one.** An exact
29
29
  * code, then a range, then the catch-all. That ordering is not a convention invented here: the
30
30
  * Responses Object says "if a response is defined using an explicit code, the explicit code
31
31
  * definition takes precedence over the range definition for that code", and `default` means every
32
32
  * status not otherwise listed. Sorting once at generation time is what keeps every application from
33
- * re-deriving the rule — and from getting it silently wrong when a `404` and a `4XX` both apply.
33
+ * re-deriving the rule - and from getting it silently wrong when a `404` and a `4XX` both apply.
34
34
  *
35
- * ⚠️ **`schema: undefined` is a statement, not an absence.** A response the document says carries no
35
+ * **`schema: undefined` is a statement, not an absence.** A response the document says carries no
36
36
  * body has no validator, and that is the truthful answer. It is also why the generated arms are
37
37
  * annotated with `satisfies readonly ResponseArm[]` rather than `as const`: measured, `as const`
38
38
  * catches a wrong `status` but is **blind** to a misspelled or omitted `schema`, because a bodyless
39
- * arm is legitimate — so the typo produces a valid-looking arm that validates nothing.
39
+ * arm is legitimate - so the typo produces a valid-looking arm that validates nothing.
40
40
  */
41
41
  export interface ResponseArm {
42
42
  /**
43
43
  * The status this arm answers with: an exact code, a `4XX`-style range, or the catch-all.
44
44
  *
45
- * ⚠️ **`"default"` is carried as itself, not resolved to a number.** OpenAPI's `default` response
45
+ * **`"default"` is carried as itself, not resolved to a number.** OpenAPI's `default` response
46
46
  * means "any status not listed", and an emitter picking one would be answering a question the
47
47
  * contract deliberately left to the application. The application maps its own failure to a status;
48
48
  * this says which schema the body must match once it has.
49
49
  *
50
- * ⚠️ **A range is carried as itself for the same reason, and it used to be dropped entirely.**
50
+ * **A range is carried as itself for the same reason, and it used to be dropped entirely.**
51
51
  * `@minValue(400) @maxValue(499) @statusCode _: int32` is a `4XX` entry in the document; the
52
52
  * emitter handled `"*"` and exact numbers and emitted nothing at all for the third case, so a
53
53
  * service could publish a whole class of failures with no arm to check them against. Expanding
54
- * `4XX` to a hundred arms would be inventing statuses the contract never listed — the range IS
54
+ * `4XX` to a hundred arms would be inventing statuses the contract never listed - the range IS
55
55
  * what the document says, and matching against it is the application's to do, once, here.
56
56
  */
57
57
  readonly status: number | "default" | `${1 | 2 | 3 | 4 | 5}XX`;
@@ -62,7 +62,7 @@ export interface ResponseArm {
62
62
  };
63
63
  }
64
64
  /**
65
- * The arm that governs a status — **the rule the OpenAPI specification states, applied once here.**
65
+ * The arm that governs a status - **the rule the OpenAPI specification states, applied once here.**
66
66
  *
67
67
  * An application knows which status it is answering with; the document knows which schema that status
68
68
  * has to match. Joining the two means resolving an overlap, because `404`, `4XX` and `default` can
@@ -71,8 +71,8 @@ export interface ResponseArm {
71
71
  * code, the explicit code definition takes precedence over the range definition for that code".
72
72
  * `default` is by definition every status not otherwise listed, so it is last.
73
73
  *
74
- * ⚠️ **Shipped beside the arms rather than left to the caller, because this is where it goes wrong.**
75
- * Left to applications it gets written as "the first arm with a status ≥ 400" — which is literally
74
+ * **Shipped beside the arms rather than left to the caller, because this is where it goes wrong.**
75
+ * Left to applications it gets written as "the first arm with a status >= 400" - which is literally
76
76
  * what one consumer's wiring did before ranges existed, and which silently skips every range arm
77
77
  * while looking correct. A package that emits an array containing `4XX` and `default` and does not
78
78
  * ship the rule for reading it has shipped a footgun.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The arm that governs a status — **the rule the OpenAPI specification states, applied once here.**
2
+ * The arm that governs a status - **the rule the OpenAPI specification states, applied once here.**
3
3
  *
4
4
  * An application knows which status it is answering with; the document knows which schema that status
5
5
  * has to match. Joining the two means resolving an overlap, because `404`, `4XX` and `default` can
@@ -8,8 +8,8 @@
8
8
  * code, the explicit code definition takes precedence over the range definition for that code".
9
9
  * `default` is by definition every status not otherwise listed, so it is last.
10
10
  *
11
- * ⚠️ **Shipped beside the arms rather than left to the caller, because this is where it goes wrong.**
12
- * Left to applications it gets written as "the first arm with a status ≥ 400" — which is literally
11
+ * **Shipped beside the arms rather than left to the caller, because this is where it goes wrong.**
12
+ * Left to applications it gets written as "the first arm with a status >= 400" - which is literally
13
13
  * what one consumer's wiring did before ranges existed, and which silently skips every range arm
14
14
  * while looking correct. A package that emits an array containing `4XX` and `default` and does not
15
15
  * ship the rule for reading it has shipped a footgun.
@@ -2,39 +2,39 @@ import type { Program, Type } from "@typespec/compiler";
2
2
  /**
3
3
  * Optional support for `@typespec/streams`.
4
4
  *
5
- * ⚠️ **Lazily imported, and absent is a normal answer.** It is a separate library most specs never
5
+ * **Lazily imported, and absent is a normal answer.** It is a separate library most specs never
6
6
  * install, so a static import would make this emitter unusable without it. The structure follows
7
- * `@typespec/openapi3`'s own `sse-module.js` — an optional peer dependency, a guarded `import()`,
8
- * and `undefined` when it is missing — because an emitter that resolves it differently from the
7
+ * `@typespec/openapi3`'s own `sse-module.js` - an optional peer dependency, a guarded `import()`,
8
+ * and `undefined` when it is missing - because an emitter that resolves it differently from the
9
9
  * reference implementation will disagree with it about which specs it can serve. openapi3 needs
10
10
  * `@typespec/events` and `@typespec/sse` as well, to build the per-event `itemSchema` OpenAPI 3.2
11
11
  * carries; this emitter only needs to know WHAT is streamed, so one is enough.
12
12
  *
13
13
  * **Why this exists at all.** An SSE operation returns `SSEStream<UnnamedEvents>`, and under
14
- * OpenAPI 3.1 its response body is `{"type": "string"}` — the event stream itself. The event
15
- * PAYLOADS (`Unnamed.Info`, `Named.ResponseCreated`, …) are published as components that no path
14
+ * OpenAPI 3.1 its response body is `{"type": "string"}` - the event stream itself. The event
15
+ * PAYLOADS (`Unnamed.Info`, `Named.ResponseCreated`, ...) are published as components that no path
16
16
  * references: openapi3 builds `itemSchema` describing them for OpenAPI 3.2, and under 3.1 emits the
17
17
  * components as a by-product. Measured: the 3.1 document mentions neither `itemSchema` nor
18
18
  * `contentSchema`, yet declares all five payload models.
19
19
  *
20
20
  * They are still part of the published vocabulary, and reachable in the TypeSpec graph even though
21
- * the 3.1 paths do not reach them — `getStreamOf` hands back whatever the stream carries, an
21
+ * the 3.1 paths do not reach them - `getStreamOf` hands back whatever the stream carries, an
22
22
  * `@events` union for SSE and the item model for JSON Lines. So the payloads get validators, and a
23
23
  * consumer streaming events has something to check each `data:` against.
24
24
  */
25
25
  export interface StreamModule {
26
- /** The type a stream carries — an `@events` union for SSE, the item model for JSON Lines. */
26
+ /** The type a stream carries - an `@events` union for SSE, the item model for JSON Lines. */
27
27
  readonly streamOf: (program: Program, type: Type) => Type | undefined;
28
28
  }
29
29
  export declare function resolveStreamModule(): Promise<StreamModule | undefined>;
30
30
  export declare function withStreamModule<T>(module: StreamModule | undefined, run: () => T): T;
31
31
  /**
32
- * What an operation streams, or `undefined` when it streams nothing — which is every operation in a
32
+ * What an operation streams, or `undefined` when it streams nothing - which is every operation in a
33
33
  * spec that does not use these libraries, and every ordinary operation in one that does.
34
34
  *
35
- * ⚠️ **Whatever the stream carries, not only an `@events` union.** The first cut required
35
+ * **Whatever the stream carries, not only an `@events` union.** The first cut required
36
36
  * `isEvents`, which fixed SSE and left `JsonlStream<Info>` behind: JSON Lines carries the item model
37
- * directly. The guard bought nothing — the streamed type is the published vocabulary in both cases —
37
+ * directly. The guard bought nothing - the streamed type is the published vocabulary in both cases -
38
38
  * and dropping it removed a second optional dependency along with the special case.
39
39
  */
40
40
  export declare function streamedTypeOf(program: Program, returnType: Type): Type | undefined;
@@ -27,12 +27,12 @@ export function withStreamModule(module, run) {
27
27
  }
28
28
  }
29
29
  /**
30
- * What an operation streams, or `undefined` when it streams nothing — which is every operation in a
30
+ * What an operation streams, or `undefined` when it streams nothing - which is every operation in a
31
31
  * spec that does not use these libraries, and every ordinary operation in one that does.
32
32
  *
33
- * ⚠️ **Whatever the stream carries, not only an `@events` union.** The first cut required
33
+ * **Whatever the stream carries, not only an `@events` union.** The first cut required
34
34
  * `isEvents`, which fixed SSE and left `JsonlStream<Info>` behind: JSON Lines carries the item model
35
- * directly. The guard bought nothing — the streamed type is the published vocabulary in both cases —
35
+ * directly. The guard bought nothing - the streamed type is the published vocabulary in both cases -
36
36
  * and dropping it removed a second optional dependency along with the special case.
37
37
  */
38
38
  export function streamedTypeOf(program, returnType) {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * What TypeSpec's compiler loads for this library.
3
3
  *
4
- * ⚠️ **There is deliberately no `$decorators` export.** This emitter's ancestor defined four —
5
- * `@trimmed`, `@loose`, `@externalValues` and `@refine` — and every one of them let the spec state
4
+ * **There is deliberately no `$decorators` export.** This emitter's ancestor defined four -
5
+ * `@trimmed`, `@loose`, `@externalValues` and `@refine` - and every one of them let the spec state
6
6
  * something `@typespec/openapi3` could not publish, so the emitted validator enforced a rule no
7
7
  * caller reading the contract could see. A decorator here is not a convenience; it is a second
8
8
  * contract. If a spec needs to say something, it says it in a way the document carries.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * What TypeSpec's compiler loads for this library.
3
3
  *
4
- * ⚠️ **There is deliberately no `$decorators` export.** This emitter's ancestor defined four —
5
- * `@trimmed`, `@loose`, `@externalValues` and `@refine` — and every one of them let the spec state
4
+ * **There is deliberately no `$decorators` export.** This emitter's ancestor defined four -
5
+ * `@trimmed`, `@loose`, `@externalValues` and `@refine` - and every one of them let the spec state
6
6
  * something `@typespec/openapi3` could not publish, so the emitted validator enforced a rule no
7
7
  * caller reading the contract could see. A decorator here is not a convenience; it is a second
8
8
  * contract. If a spec needs to say something, it says it in a way the document carries.
@@ -1,10 +1,19 @@
1
1
  import { type ModelProperty, type Program, type Type } from "@typespec/compiler";
2
- /** A property line, including its optionality marker. */
3
- export declare function propertyToTs(program: Program, property: ModelProperty): string;
2
+ /**
3
+ * A property line, including its optionality marker.
4
+ *
5
+ * `wireName` overrides the key for an HTTP PARAMETER, whose wire name comes from `@header("x-thing")`
6
+ * or `@query("$select")` rather than from any JSON encoding. `resolveEncodedName` cannot see those,
7
+ * so a header renamed on the wire was keyed here by its TypeScript property name while the emitted
8
+ * validator keyed it by the wire name. A consumer checking its handlers against the generated input
9
+ * types then failed to typecheck against a server that was correct: `z.object({ "x-thing": ... })`
10
+ * on one side and `{ thing: string }` on the other.
11
+ */
12
+ export declare function propertyToTs(program: Program, property: ModelProperty, wireName?: string): string;
4
13
  export declare function withTsRefResolver<T>(resolver: (type: Type) => string | undefined, run: () => T): T;
5
14
  export declare function typeToTs(program: Program, type: Type): string;
6
15
  /**
7
- * The same walk, except that `type` is never resolved to its own name — see `SchemaRegistry.#inline`
16
+ * The same walk, except that `type` is never resolved to its own name - see `SchemaRegistry.#inline`
8
17
  * for why the "not itself" rule has to sit at the top of the walk rather than inside the resolver.
9
18
  */
10
19
  export declare function typeToTsBody(program: Program, type: Type): string;