typespec-http-zod 0.1.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.
@@ -0,0 +1,254 @@
1
+ import { getDiscriminatedUnion, getEncode, resolveEncodedName, } from "@typespec/compiler";
2
+ import { reportDiagnostic } from "./lib.js";
3
+ import { discriminatedSubtypes, effectiveIndexer, inheritedAndOwnProperties, objectKey, } from "./zod.js";
4
+ /**
5
+ * TypeSpec type → plain TypeScript type text.
6
+ *
7
+ * **Why a second walker rather than `z.infer` of the emitted Zod.** The contract types are meant to
8
+ * be framework-free: a codebase passes them between layers that have no business importing a
9
+ * validation library, and layering rules commonly forbid exactly that. A type inferred from a schema
10
+ * drags the schema — and `zod` — everywhere it goes, so the shared type has to be emitted as ordinary
11
+ * TypeScript. That is the whole point: one declaration both sides are checked against, with neither
12
+ * importing the other.
13
+ *
14
+ * **It must agree with `zod.ts` exactly**, because the emitted assertion compares them. Every rule
15
+ * here has a counterpart there: `utcDateTime` is a string on both sides, `?` becomes
16
+ * `?: T | undefined` because `exactOptionalPropertyTypes` is on, and a defaulted property is
17
+ * **required** here because `z.infer` reports the *output* type and a default has always fired by
18
+ * then.
19
+ */
20
+ /** Scalars we accept, and the TypeScript they become. Mirrors `SCALARS` in `zod.ts`. */
21
+ const SCALARS = {
22
+ string: "string",
23
+ boolean: "boolean",
24
+ bytes: "string",
25
+ int8: "number",
26
+ int16: "number",
27
+ int32: "number",
28
+ int64: "number",
29
+ safeint: "number",
30
+ uint8: "number",
31
+ uint16: "number",
32
+ uint32: "number",
33
+ uint64: "number",
34
+ integer: "number",
35
+ float: "number",
36
+ float32: "number",
37
+ float64: "number",
38
+ decimal: "number",
39
+ decimal128: "number",
40
+ numeric: "number",
41
+ /** Dates cross the wire as strings — `zod.ts` emits `z.string()` for all of these. */
42
+ utcDateTime: "string",
43
+ offsetDateTime: "string",
44
+ plainDate: "string",
45
+ plainTime: "string",
46
+ duration: "string",
47
+ url: "string",
48
+ };
49
+ /**
50
+ * What a walk returns after reporting a refusal — see `UNREPRESENTABLE` in `zod.ts`.
51
+ *
52
+ * `never` is the honest placeholder in TypeScript: nothing is assignable to it, so a contract type
53
+ * built on one cannot be used by accident, and the error diagnostic has already failed the compile.
54
+ */
55
+ const UNREPRESENTABLE = "never";
56
+ /** Refuse `type`, pointing at its declaration, and carry on so the rest is reported too. */
57
+ function refuse(program, type, why) {
58
+ reportDiagnostic(program, {
59
+ code: "unsupported-type",
60
+ target: type,
61
+ format: { artefact: "a TypeScript type", kind: type.kind, why },
62
+ });
63
+ return UNREPRESENTABLE;
64
+ }
65
+ function scalarToTs(program, scalar) {
66
+ // A scalar declaration can carry the encoding itself — see `scalarToZod`.
67
+ const encoded = getEncode(program, scalar)?.type;
68
+ if (encoded !== undefined && encoded !== scalar)
69
+ return scalarToTs(program, encoded);
70
+ let current = scalar;
71
+ // Walk `extends` so `scalar TrimmedString extends string` resolves to the primitive underneath.
72
+ while (current !== undefined) {
73
+ const mapped = SCALARS[current.name];
74
+ if (mapped !== undefined)
75
+ return mapped;
76
+ current = current.baseScalar;
77
+ }
78
+ reportDiagnostic(program, {
79
+ code: "unsupported-scalar",
80
+ target: scalar,
81
+ format: { artefact: "a TypeScript type", name: scalar.name },
82
+ });
83
+ return UNREPRESENTABLE;
84
+ }
85
+ function enumToTs(target) {
86
+ const values = [...target.members.values()].map((member) => member.value ?? member.name);
87
+ return values.map((value) => JSON.stringify(value)).join(" | ");
88
+ }
89
+ function unionToTs(program, union) {
90
+ /**
91
+ * A `@discriminated` union is a choice between WRAPPINGS of its variants — see
92
+ * `discriminatedVariant` in `zod.ts` for the two wire shapes and why `envelope: "none"` diverges
93
+ * from the document. Emitted here too, or the contract type says `Cat | Dog` while the validator
94
+ * requires `{kind, value}` and the assertion pairing them fails.
95
+ */
96
+ const [discriminated] = getDiscriminatedUnion(program, union);
97
+ if (discriminated !== undefined && discriminated.defaultVariant === undefined) {
98
+ const { envelope, discriminatorPropertyName, envelopePropertyName } = discriminated.options;
99
+ const arms = [...discriminated.variants.entries()].map(([name, variant]) => {
100
+ const tag = `${objectKey(discriminatorPropertyName)}: ${JSON.stringify(name)};`;
101
+ const inner = typeToTs(program, variant);
102
+ // No injection: with no envelope the variant declares the discriminator itself, or the union
103
+ // is refused in `zod.ts`. Nothing is added that the document does not publish.
104
+ return envelope === "none"
105
+ ? inner
106
+ : `{ ${tag} ${objectKey(envelopePropertyName)}: ${inner}; }`;
107
+ });
108
+ return arms.join(" | ");
109
+ }
110
+ const variants = [...union.variants.values()].map((variant) => variant.type);
111
+ if (variants.length === 0) {
112
+ reportDiagnostic(program, {
113
+ code: "empty-union",
114
+ target: union,
115
+ format: { why: "it is empty" },
116
+ });
117
+ return UNREPRESENTABLE;
118
+ }
119
+ return variants.map((type) => typeToTs(program, type)).join(" | ");
120
+ }
121
+ function modelToTs(program, model) {
122
+ /**
123
+ * ⚠️ **A `@discriminator` base is a CHOICE between its subtypes, not a shape of its own — and this
124
+ * walk did not know that.** `modelToZod` has had the rule since polymorphism was first supported;
125
+ * here the base was walked as an ordinary model, so `@discriminator("kind") model Shape` produced
126
+ * `interface Shape { kind: string; label: string }`.
127
+ *
128
+ * Three things were wrong with that at once. The type could not be discriminated, so no consumer
129
+ * could narrow it; `kind: string` admitted every string the validator rejects; and the subtypes got
130
+ * no declaration at all, because nothing reached them.
131
+ *
132
+ * ⚠️ **It survived because the two walks are only ever compared where a spec declares polymorphism
133
+ * AND something compiles the emitted assertions.** `wire-contract.gen.ts` exists precisely to catch
134
+ * the emitter disagreeing with itself — one TypeSpec, walked twice — and this is the second defect
135
+ * of that class it has found. The first was a vocabulary inlined by one walk and referenced by the
136
+ * other.
137
+ */
138
+ const subtypes = discriminatedSubtypes(program, model);
139
+ if (subtypes.length > 0) {
140
+ return subtypes.map((derived) => typeToTs(program, derived)).join(" | ");
141
+ }
142
+ const indexer = effectiveIndexer(model);
143
+ const indexerKey = indexer?.key.name;
144
+ // Inherited properties included — see `inheritedAndOwnProperties`. Both artefacts describe the
145
+ // same model, so a base whose properties reach one and not the other is a contradiction we ship.
146
+ const declared = inheritedAndOwnProperties(model);
147
+ if (indexerKey === "integer" && indexer !== undefined) {
148
+ const element = typeToTs(program, indexer.value);
149
+ // Parenthesised so `A | B` as an element does not become `A | B[]`.
150
+ return /[|&\s]/.test(element) ? `(${element})[]` : `${element}[]`;
151
+ }
152
+ /**
153
+ * ⚠️ **A string indexer only means "dictionary" when the model declares NO properties.**
154
+ *
155
+ * The same trap `modelToZod` had, and it bites harder here: every permissive response model is
156
+ * `{ …pinned fields; ...Record<unknown> }`, so reading the indexer first would type all 97 of them
157
+ * as a bare `Record<string, unknown>` — a type that satisfies any assertion about what the response
158
+ * contains, which is precisely the check this file exists to feed.
159
+ */
160
+ if (indexerKey === "string" && indexer !== undefined && declared.length === 0) {
161
+ return `Record<string, ${typeToTs(program, indexer.value)}>`;
162
+ }
163
+ const entries = declared.map((property) => `\t${propertyToTs(program, property)}`);
164
+ /**
165
+ * ⚠️ **The indexer is deliberately NOT intersected in.**
166
+ *
167
+ * `{ …pinned } & Record<string, unknown>` looks like the honest rendering of a permissive model,
168
+ * and it makes the type unsatisfiable: a plain interface has no index signature, so no domain
169
+ * result can be assignable to it and the assertion this type feeds fails for **every** operation.
170
+ * A gate that fails for everything is as useless as one that passes for everything.
171
+ *
172
+ * Permissiveness is a statement about the VALIDATOR — what it lets through — not about what a
173
+ * producer must contain. The contract type is the floor: these fields, at least.
174
+ */
175
+ return entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
176
+ }
177
+ /** A property line, including its optionality marker. */
178
+ export function propertyToTs(program, property) {
179
+ // The WIRE name, resolved exactly as `zod.ts` resolves it — `wire-contract.gen.ts` asserts the
180
+ // emitted Zod infers this type, so the two must name every property identically or it fails.
181
+ const key = objectKey(resolveEncodedName(program, property, "application/json"));
182
+ // `@encode` changes the wire TYPE, resolved exactly as `zod.ts` resolves it — a property encoded as
183
+ // a string on one side and an array on the other fails the emitted assertion, and deserves to.
184
+ const encoded = getEncode(program, property)?.type;
185
+ const value = typeToTs(program, encoded ?? property.type);
186
+ /**
187
+ * ⚠️ **A defaulted property is REQUIRED here.**
188
+ *
189
+ * `zod.ts` emits `.default(v)`, which makes the field optional on the way *in* and guaranteed on
190
+ * the way *out*. The assertion this type feeds compares against `z.infer`, which is the output
191
+ * type — so by the time a value reaches the wire the default has fired and the field is present.
192
+ * Marking it optional would make a correct schema fail its own assertion.
193
+ */
194
+ if (property.defaultValue !== undefined)
195
+ return `${key}: ${value};`;
196
+ // `exactOptionalPropertyTypes` is on, and Zod's `.optional()` infers `T | undefined`.
197
+ return property.optional ? `${key}?: ${value} | undefined;` : `${key}: ${value};`;
198
+ }
199
+ /** Installed by the registry so a named model is declared once and referenced everywhere else. */
200
+ let resolveRef = () => undefined;
201
+ export function withTsRefResolver(resolver, run) {
202
+ const previous = resolveRef;
203
+ resolveRef = resolver;
204
+ try {
205
+ return run();
206
+ }
207
+ finally {
208
+ resolveRef = previous;
209
+ }
210
+ }
211
+ export function typeToTs(program, type) {
212
+ const ref = resolveRef(type);
213
+ if (ref !== undefined)
214
+ return ref;
215
+ return typeToTsBody(program, type);
216
+ }
217
+ /**
218
+ * The same walk, except that `type` is never resolved to its own name — see `SchemaRegistry.#inline`
219
+ * for why the "not itself" rule has to sit at the top of the walk rather than inside the resolver.
220
+ */
221
+ export function typeToTsBody(program, type) {
222
+ switch (type.kind) {
223
+ case "Scalar":
224
+ return scalarToTs(program, type);
225
+ case "Model":
226
+ return modelToTs(program, type);
227
+ case "Enum":
228
+ return enumToTs(type);
229
+ case "Union":
230
+ return unionToTs(program, type);
231
+ case "String":
232
+ return JSON.stringify(type.value);
233
+ case "Number":
234
+ return String(type.value);
235
+ case "Boolean":
236
+ return String(type.value);
237
+ case "EnumMember":
238
+ return JSON.stringify(type.value ?? type.name);
239
+ // A named union variant is a referable type — `DogKind.Golden`. See the note in `zod.ts`.
240
+ case "UnionVariant":
241
+ return typeToTs(program, type.type);
242
+ case "Intrinsic":
243
+ if (type.name === "null")
244
+ return "null";
245
+ if (type.name === "unknown")
246
+ return "unknown";
247
+ if (type.name === "void" || type.name === "never") {
248
+ return refuse(program, type, `\`${type.name}\` has no runtime representation`);
249
+ }
250
+ return refuse(program, type, `intrinsic "${type.name}"`);
251
+ default:
252
+ return refuse(program, type, "no rule for this kind");
253
+ }
254
+ }
@@ -0,0 +1,58 @@
1
+ import { type HttpService } from "@typespec/http";
2
+ import { type Program } from "@typespec/compiler";
3
+ /**
4
+ * **Serve the version the service is on, not every version it has ever had.**
5
+ *
6
+ * ⚠️ **The emitter used to emit the UNION of all versions.** Measured on
7
+ * `versioning/removed`: v2 deletes `POST /v1` and `POST /interface-v1/v1`, and both were mounted
8
+ * anyway — 582 routes against the 580 the document declares. A service built on that output serves
9
+ * endpoints its current version does not have, validates bodies against properties a later version
10
+ * removed, and does all of it while the document says otherwise.
11
+ *
12
+ * It stayed hidden because the oracle was reading the wrong file: `versioning/removed` declares
13
+ * `v1, v2preview, v2`, the documents sort alphabetically, and the differential compared us against
14
+ * `openapi.v2preview.json`. Fixing WHICH document is read is what made this visible at all.
15
+ *
16
+ * ⚠️ **`@typespec/versioning` is an OPTIONAL peer, behind a guarded `import()`** — the same treatment
17
+ * `@typespec/streams` gets, and for the same reason: a spec that declares no versions must not need
18
+ * the package installed. `@typespec/openapi3` resolves it exactly this way (`versioning-module.js`),
19
+ * so a service is versioned for us precisely when it is versioned for the document we are judged
20
+ * against.
21
+ */
22
+ interface VersionSnapshot {
23
+ readonly version?: {
24
+ readonly value: string;
25
+ };
26
+ readonly mutator: unknown;
27
+ }
28
+ interface VersioningModule {
29
+ readonly getVersioningMutators: (program: Program, namespace: unknown) => {
30
+ readonly kind: "transient";
31
+ readonly mutator: unknown;
32
+ } | {
33
+ readonly kind: "versioned";
34
+ readonly snapshots: readonly VersionSnapshot[];
35
+ } | undefined;
36
+ }
37
+ export declare function resolveVersioningModule(): Promise<VersioningModule | undefined>;
38
+ /** A service to emit, and the version it represents where there is one. */
39
+ export interface ServiceSnapshot {
40
+ readonly service: HttpService;
41
+ /** The version this snapshot is of. `undefined` for an unversioned service. */
42
+ readonly version?: string;
43
+ }
44
+ /**
45
+ * Every service to emit, each already projected to its current version.
46
+ *
47
+ * ⚠️ **The LAST snapshot, not a merge and not a choice.** `getVersioningMutators` returns snapshots
48
+ * in declared order — which is the only order that means anything, since a version name sorts
49
+ * however its author spelled it (`v2preview` sorts after `v2`). The last one is what the service
50
+ * currently serves, and one schema set can only describe one version.
51
+ *
52
+ * Serving an older version would be a legitimate thing to want and is deliberately NOT offered here:
53
+ * it needs an option, a name for the emitted directory, and an answer for what the contracts package
54
+ * exports. Emitting the union — which is what happened before — is not that feature, it is the
55
+ * absence of this one.
56
+ */
57
+ export declare function serviceSnapshots(program: Program, versioning: VersioningModule | undefined): ServiceSnapshot[];
58
+ export {};
@@ -0,0 +1,57 @@
1
+ import { getAllHttpServices, getHttpService } from "@typespec/http";
2
+ import { listServices } from "@typespec/compiler";
3
+ import { unsafe_mutateSubgraphWithNamespace } from "@typespec/compiler/experimental";
4
+ export async function resolveVersioningModule() {
5
+ try {
6
+ return (await import("@typespec/versioning"));
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
12
+ /**
13
+ * Every service to emit, each already projected to its current version.
14
+ *
15
+ * ⚠️ **The LAST snapshot, not a merge and not a choice.** `getVersioningMutators` returns snapshots
16
+ * in declared order — which is the only order that means anything, since a version name sorts
17
+ * however its author spelled it (`v2preview` sorts after `v2`). The last one is what the service
18
+ * currently serves, and one schema set can only describe one version.
19
+ *
20
+ * Serving an older version would be a legitimate thing to want and is deliberately NOT offered here:
21
+ * it needs an option, a name for the emitted directory, and an answer for what the contracts package
22
+ * exports. Emitting the union — which is what happened before — is not that feature, it is the
23
+ * absence of this one.
24
+ */
25
+ export function serviceSnapshots(program, versioning) {
26
+ const [unversioned] = getAllHttpServices(program);
27
+ if (versioning === undefined)
28
+ return unversioned.map((service) => ({ service }));
29
+ const snapshots = [];
30
+ for (const service of listServices(program)) {
31
+ const mutators = versioning.getVersioningMutators(program, service.type);
32
+ if (mutators === undefined) {
33
+ // Not versioned: the service as written is the service that runs.
34
+ const match = unversioned.find((candidate) => candidate.namespace === service.type);
35
+ if (match !== undefined)
36
+ snapshots.push({ service: match });
37
+ continue;
38
+ }
39
+ const mutator = mutators.kind === "transient" ? mutators.mutator : mutators.snapshots.at(-1)?.mutator;
40
+ const version = mutators.kind === "transient" ? undefined : mutators.snapshots.at(-1)?.version?.value;
41
+ if (mutator === undefined)
42
+ continue;
43
+ const projected = projectService(program, service.type, mutator);
44
+ if (projected !== undefined) {
45
+ snapshots.push(version === undefined ? { service: projected } : { service: projected, version });
46
+ }
47
+ }
48
+ // A versioned spec whose mutators produced nothing must not silently emit nothing at all.
49
+ return snapshots.length === 0 ? unversioned.map((service) => ({ service })) : snapshots;
50
+ }
51
+ function projectService(program, namespace, mutator) {
52
+ const subgraph = unsafe_mutateSubgraphWithNamespace(program, [mutator], namespace);
53
+ if (subgraph.type.kind !== "Namespace")
54
+ return undefined;
55
+ const [service] = getHttpService(program, subgraph.type);
56
+ return service;
57
+ }
@@ -0,0 +1,124 @@
1
+ import { type Enum, type Model, type ModelProperty, type Program, type Type } from "@typespec/compiler";
2
+ import { Visibility } from "@typespec/http";
3
+ export declare function withSealedObjects<T>(sealed: boolean, run: () => T): T;
4
+ export declare function withVisibility<T>(program: Program, next: Visibility, run: () => T): T;
5
+ /** Whether this model is shaped differently at `visibility` than at the canonical `Read`. */
6
+ export declare function isTransformedBy(program: Program, model: Model, at: Visibility): boolean;
7
+ /** The visibility the current walk is emitting for — the registry keys declarations on it. */
8
+ export declare function currentVisibility(): Visibility;
9
+ /**
10
+ * Record every enum a service DECLARES, not merely every one its success bodies happen to reach.
11
+ *
12
+ * ⚠️ **A vocabulary is a fact about the contract, not about a code path.** Collecting them during the
13
+ * Zod walk meant an enum reachable only through an ERROR body was never emitted — which is exactly
14
+ * what happened to one surface's error-code enum: declared in the spec, published on every
15
+ * operation's failure responses, and absent from the generated vocabularies, so the one list a
16
+ * consumer branches on had to be hand-written beside the generated ones. Walking declarations removes
17
+ * the whole class rather than the instance.
18
+ */
19
+ export declare function noteDeclaredVocabularies(enums: Iterable<Enum>): void;
20
+ /**
21
+ * Record a named model's PROPERTY NAMES as a runtime vocabulary.
22
+ *
23
+ * ⚠️ **This exists because "a `Record` keyed by an enum" is inexpressible, and the workaround was
24
+ * about to cost a duplicated list.** `ProfileAttributes` is 150-odd known keys; naming them in the
25
+ * spec is what makes the published document state the constraint instead of hiding it behind a
26
+ * hand-written predicate. But a *type* cannot be iterated, and more than one layer usually needs
27
+ * the set at RUN time — so without this the keys would live in the spec AND as a hand-maintained
28
+ * tuple in a shared package — one vocabulary stated twice, which is precisely the
29
+ * drift the deleted predicate existed to prevent.
30
+ *
31
+ * ⚠️ **Selected by an emitter OPTION, deliberately not by a decorator.** A decorator would put a
32
+ * mark in the spec that no document carries — the exact objection that deleted the last four. This
33
+ * changes no schema, states nothing in the contract and enforces nothing; it decides which
34
+ * convenience artefact gets written beside the types, which is what an emitter option is for.
35
+ *
36
+ * Wire names, like everything else here: the document publishes one name per property, and a
37
+ * consumer checking a key against this tuple is checking what arrived.
38
+ */
39
+ export declare function noteKeyVocabularies(program: Program, models: Iterable<Model>, wanted: readonly string[]): string[];
40
+ /** Record that the emitted schemas reference `name` from `module`. */
41
+ export declare function noteExternalImport(module: string, name: string): void;
42
+ export declare function withContractsPackage<T>(name: string | undefined, run: () => T): T;
43
+ /**
44
+ * Run `walk` with a fresh import collector, and hand back what it referenced.
45
+ *
46
+ * Throwing rather than lazily creating the map is deliberate: a walk that runs outside a collector
47
+ * is a wiring mistake whose only symptom would be a generated file missing an import, which the
48
+ * emitted TypeScript reports as an undefined name far from the cause.
49
+ */
50
+ export declare function collectExternalImports<T>(walk: () => T): {
51
+ readonly result: T;
52
+ readonly imports: Map<string, Set<string>>;
53
+ readonly vocabularies: Map<string, readonly string[]>;
54
+ };
55
+ export declare function withSyntheticDeclarations<T>(hook: (name: string, build: () => string) => string, run: () => T): T;
56
+ /**
57
+ * Every property the model declares, **including the ones it inherits**.
58
+ *
59
+ * ⚠️ **`model.properties` holds only a model's OWN properties, and `baseModel` appeared nowhere in
60
+ * this emitter.** `model Extension extends Element` reaches the document as `allOf: [{$ref: Element}]`
61
+ * beside the derived model's own properties, and the sealing keyword openapi3 uses —
62
+ * `unevaluatedProperties` — is chosen precisely because it sees through that. So the document said
63
+ * `Extension` carries `level` **and** the inherited `extension`, while this emitter produced
64
+ * `z.object({level}).strict()`: the inherited property gone, and the model closed against it. The
65
+ * conformance scenario's own documented request body would have been rejected with a 400 naming a key
66
+ * the contract requires.
67
+ *
68
+ * Invisible for two reasons at once. The spec it was first built against declared no `extends` at
69
+ * all, and the differential
70
+ * skipped every schema carrying an `allOf` — so 28 of 226 components were not divergent, they were
71
+ * never compared.
72
+ *
73
+ * Flattened rather than composed. `z.object(...).extend(...)` would mirror the document's structure
74
+ * more literally and cannot be used here: `.extend()` reads `shape` eagerly, so a base whose property
75
+ * refers back to the derived model — `type/model/inheritance/recursive`, the exact case — would throw
76
+ * during module initialisation. Flattening asks nothing of the base but its properties.
77
+ */
78
+ export declare function inheritedAndOwnProperties(model: Model): ModelProperty[];
79
+ /** The indexer in force, which a model can inherit — `model X extends Record<unknown>`. */
80
+ export declare function effectiveIndexer(model: Model): Model["indexer"];
81
+ /**
82
+ * The subtypes a `@discriminator` base stands for, in declaration order.
83
+ *
84
+ * Filtered by openapi3's own {@link includeDerivedModel} so a template declaration is not mistaken
85
+ * for a real subtype — the same rule that decides whether a base may be sealed.
86
+ */
87
+ export declare function discriminatedSubtypes(program: Program, model: Model): readonly Model[];
88
+ /**
89
+ * A name as an object key — quoted when it is not a legal JavaScript identifier.
90
+ *
91
+ * ⚠️ **A TypeSpec property can be named anything**, and the corpus proves it: `parameters/spread`
92
+ * declares `` `x-ms-test-header`: string ``. A parameter walk assumed the property name was always
93
+ * safe — its docblock even argued that using the property name rather than the wire name was what
94
+ * avoided this — and emitted `x-ms-test-header: z.string()`, a file that does not parse.
95
+ *
96
+ * Exported so there is one answer to this question. There were two sites and only one of them knew.
97
+ */
98
+ export declare function objectKey(name: string): string;
99
+ export declare function propertyToZod(program: Program, property: ModelProperty): string;
100
+ export declare function withRefResolver<T>(resolver: (type: Type) => string | undefined, run: () => T): T;
101
+ /** Called by the registry when it hands out the identifier of a declaration that is still open. */
102
+ export declare function noteBackEdge(): void;
103
+ /**
104
+ * Render `run()`, reporting whether it reached back into a declaration still being rendered.
105
+ *
106
+ * ⚠️ **The count is restored on the way out, and that is the point.** A caller that wraps the
107
+ * reference in a getter has *settled* the back edge — the reference is now lazy and the enclosing
108
+ * object is safe. Letting it propagate would make every ancestor defer as well, and would make the
109
+ * registry report a cycle that has already been dealt with.
110
+ */
111
+ export declare function captureBackEdges<T>(run: () => T): {
112
+ readonly value: T;
113
+ readonly deferred: boolean;
114
+ };
115
+ export declare function typeToZod(program: Program, type: Type): string;
116
+ /**
117
+ * The same walk, except that `type` is never resolved to its own identifier.
118
+ *
119
+ * The registry renders a declaration's *body* with this: asking the resolver first would hand back
120
+ * the identifier the declaration is about to bind, emitting `const xSchema = xSchema`. Nested
121
+ * occurrences still go through {@link typeToZod} and do resolve — which is what makes a recursive
122
+ * model reference itself instead of inlining itself forever.
123
+ */
124
+ export declare function typeToZodBody(program: Program, type: Type): string;