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.
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/dist/src/api.d.ts +304 -0
- package/dist/src/api.js +1425 -0
- package/dist/src/constraints.d.ts +38 -0
- package/dist/src/constraints.js +68 -0
- package/dist/src/emitter.d.ts +13 -0
- package/dist/src/emitter.js +15 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/lib.d.ts +181 -0
- package/dist/src/lib.js +172 -0
- package/dist/src/registry.d.ts +69 -0
- package/dist/src/registry.js +421 -0
- package/dist/src/runtime.d.ts +84 -0
- package/dist/src/runtime.js +25 -0
- package/dist/src/streams.d.ts +40 -0
- package/dist/src/streams.js +40 -0
- package/dist/src/tsp-index.d.ts +12 -0
- package/dist/src/tsp-index.js +12 -0
- package/dist/src/types.d.ts +10 -0
- package/dist/src/types.js +254 -0
- package/dist/src/versioning.d.ts +58 -0
- package/dist/src/versioning.js +57 -0
- package/dist/src/zod.d.ts +124 -0
- package/dist/src/zod.js +1015 -0
- package/lib/main.tsp +4 -0
- package/package.json +94 -0
|
@@ -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;
|