typespec-http-zod 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/src/api.d.ts +31 -0
- package/dist/src/api.js +1443 -360
- package/dist/src/constraints.d.ts +31 -2
- package/dist/src/constraints.js +242 -46
- package/dist/src/lib.d.ts +136 -13
- package/dist/src/lib.js +167 -18
- package/dist/src/published.d.ts +139 -0
- package/dist/src/published.js +798 -0
- package/dist/src/registry.d.ts +77 -45
- package/dist/src/registry.js +759 -198
- package/dist/src/types.d.ts +12 -2
- package/dist/src/types.js +267 -92
- package/dist/src/uri-template.d.ts +110 -8
- package/dist/src/uri-template.js +251 -46
- package/dist/src/zod.d.ts +715 -39
- package/dist/src/zod.js +4029 -547
- package/package.json +1 -1
package/dist/src/registry.js
CHANGED
|
@@ -1,72 +1,13 @@
|
|
|
1
1
|
import { getDoc, getFriendlyName, } from "@typespec/compiler";
|
|
2
2
|
import { getVisibilitySuffix, Visibility } from "@typespec/http";
|
|
3
|
+
import { firstReportOf } from "./constraints.js";
|
|
3
4
|
import { reportDiagnostic } from "./lib.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { isSplitByVisibility, isSuffixedByUsage, viewRemedyOf, viewTemplateOf, } from "./published.js";
|
|
6
|
+
import { propertyToTs, propertyTypeToTs, typeToTs, typeToTsBody, withTsRefResolver, withTsSyntheticDeclarations, } from "./types.js";
|
|
7
|
+
import { captureBackEdges, currentVisibility, described, externalReferenceOf, isArrayLiteral, isOptionalAt, isSpeakableInstance, refuseExternalReference, metadataAnnotationsIgnored, metadataInfoOf, multipartBinaryContext, noteBackEdge, OWN_POSITION, propertyToZod, renderedVisibilityOf, rootVisibilityOf, UNREPRESENTABLE, typeToZodBody, visibilityName, withDeclarationSite, withDeferredAnnotator, withMetadataAnnotationsIgnored, withMultipartBinary, withRefResolver, withHeldDeclarations, withSyntheticDeclarations, withParameterPosition, withVisibility, } from "./zod.js";
|
|
6
8
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
* **`@typespec/http` hands back a synthesised model for a body whose declaration carried
|
|
10
|
-
* metadata**, and it has no name - so `@error model InvalidAuth { @statusCode _: 403; error: string }`
|
|
11
|
-
* reached the registry as `{error}` with `name: ""`, was inlined at its use site, and produced **no
|
|
12
|
-
* validator at all** for a component the document publishes as `InvalidAuth`. Four authentication
|
|
13
|
-
* scenarios, each with an unvalidated 403.
|
|
14
|
-
*
|
|
15
|
-
* `@typespec/openapi3` never meets that model: it works from the TypeSpec type and strips metadata
|
|
16
|
-
* itself, which is why its component keeps the original name. This recovers the same name from
|
|
17
|
-
* `sourceModels`, so both artefacts call the shape the same thing.
|
|
18
|
-
*
|
|
19
|
-
* **Only a PURE spread of one named model.** `{...A, extra: string}` is a different shape and must
|
|
20
|
-
* not claim `A`'s name - so every property has to come from the source. Metadata-stripping removes
|
|
21
|
-
* properties and never adds them, which is exactly the asymmetry this tests for. A collision would be
|
|
22
|
-
* caught by the differential's `component-name-collision` arm regardless.
|
|
23
|
-
*/
|
|
24
|
-
function spreadSourceOf(type, seen = new Set(),
|
|
25
|
-
/**
|
|
26
|
-
* The properties the ORIGINAL type has, carried down the chain rather than re-derived.
|
|
27
|
-
*
|
|
28
|
-
* **Re-deriving them at each link is what blocked `Alias.InnerModel`.** The intermediate model
|
|
29
|
-
* an operation builds holds the spread body's properties AND the operation's own `@path` and
|
|
30
|
-
* `@header` parameters, so checking THAT set against the named ancestor fails on properties the
|
|
31
|
-
* body never had. The question is whether every property of the shape being named comes from the
|
|
32
|
-
* ancestor - `{name}` does - not whether some intermediate did.
|
|
33
|
-
*
|
|
34
|
-
* The guard is unchanged in what it refuses: `{...A, extra}` still fails, because `extra` is in
|
|
35
|
-
* the original set and not in `A`.
|
|
36
|
-
*/
|
|
37
|
-
required = [...type.properties.keys()]) {
|
|
38
|
-
if (seen.has(type))
|
|
39
|
-
return undefined;
|
|
40
|
-
const sources = type.sourceModels;
|
|
41
|
-
if (sources.length !== 1)
|
|
42
|
-
return undefined;
|
|
43
|
-
const source = sources[0];
|
|
44
|
-
if (source === undefined || source.usage !== "spread")
|
|
45
|
-
return undefined;
|
|
46
|
-
const model = source.model;
|
|
47
|
-
if (model.templateMapper !== undefined)
|
|
48
|
-
return undefined;
|
|
49
|
-
for (const property of required) {
|
|
50
|
-
if (!model.properties.has(property))
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* **Follow the chain through ANONYMOUS links.** TypeSpec puts an unnamed model between an
|
|
55
|
-
* operation's parameter model and the model that was spread into it, so stopping at the first
|
|
56
|
-
* source finds `""` and gives up - measured: the bodies for `Alias.InnerModel` and
|
|
57
|
-
* `Model.CompositeRequestMix` both reported `sources: [{ usage: "spread", name: "" }]` while the
|
|
58
|
-
* document published both as components, and the one case that already worked
|
|
59
|
-
* (`Model.BodyParameter`) was the one whose source happened to be named directly.
|
|
60
|
-
*
|
|
61
|
-
* Terminating on an unnamed source with no named ancestor is what keeps a spread ALIAS inline:
|
|
62
|
-
* OpenAPI names a component for a spread model and not for an alias, and following the chain
|
|
63
|
-
* reproduces that distinction rather than asserting it.
|
|
64
|
-
*/
|
|
65
|
-
return model.name === "" ? spreadSourceOf(model, new Set([...seen, type]), required) : model;
|
|
66
|
-
}
|
|
67
|
-
const spreadSourceName = (type) => spreadSourceOf(type)?.name;
|
|
68
|
-
/**
|
|
69
|
-
* ECMAScript's reserved words, which cannot name a TypeScript declaration.
|
|
9
|
+
* The words TypeScript refuses as the name of a declaration the emitted files write - a type alias,
|
|
10
|
+
* an interface, and the references to each - in a module, which is strict mode.
|
|
70
11
|
*
|
|
71
12
|
* **A model may be named after one, and the corpus declares exactly that on purpose.** `special-words`
|
|
72
13
|
* carries `await`, `break` and the rest, and this emitter wrote `export interface await { ... }` and
|
|
@@ -75,25 +16,28 @@ const spreadSourceName = (type) => spreadSourceOf(type)?.name;
|
|
|
75
16
|
*
|
|
76
17
|
* **This is a LIST, which this package otherwise refuses.** It is defensible here because it is
|
|
77
18
|
* not a list about our own code that drifts as the code changes: it is a closed set the language
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* Most contextual keywords are absent, because they are legal: `interface`, `string`, `number`,
|
|
83
|
-
* `any`, `never` and `public` all name a type without complaint.
|
|
19
|
+
* fixes. The guard is that `test/round9/names/` reads every keyword from the scanner of the
|
|
20
|
+
* TypeScript this package builds with, measures which ones `tsc` refuses in each position, and holds
|
|
21
|
+
* this set to exactly those - so a word missing here, or one renamed that need not be, fails a test.
|
|
84
22
|
*
|
|
85
|
-
* **
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
23
|
+
* **It is four groups, and only the first is ECMAScript's reserved words.** This docblock used to say
|
|
24
|
+
* `interface`, `string`, `number`, `any`, `never` and `public` name a type without complaint; every
|
|
25
|
+
* one of them is refused, and `export type object = ...` did not even load (`Type alias name cannot
|
|
26
|
+
* be 'object'`). Measured against tsc 7.0.2 over the 84 keywords its scanner knows, as a model, an
|
|
27
|
+
* enum and a union: 61 refused, the 61 below; the other 23 - `type`, `declare`, `module`, `of` and
|
|
28
|
+
* the like - are legal everywhere the files write a name.
|
|
90
29
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
30
|
+
* - ECMAScript's reserved words, and `await` and `yield`, which a module reserves. `as` is contextual
|
|
31
|
+
* but `export type as = ...` is `TS1005`.
|
|
32
|
+
* - The words strict mode reserves: `implements`, `interface`, `let`, `package`, `private`,
|
|
33
|
+
* `protected`, `public`, `static` - `TS1214`, since a module is strict.
|
|
34
|
+
* - The predefined types, which no declaration may be named: `any`, `bigint`, `boolean`, `never`,
|
|
35
|
+
* `number`, `object`, `string`, `symbol`, `undefined`, `unknown` - `TS2457` and `TS2427`.
|
|
36
|
+
* - The type operators, which do not parse where a type is referred to: `p?: keyof;` is `TS1110`,
|
|
37
|
+
* and so are `readonly` and `unique`; `infer` is `TS1003`.
|
|
95
38
|
*/
|
|
96
39
|
const RESERVED_DECLARATION_NAMES = new Set([
|
|
40
|
+
// ECMAScript's reserved words, and the two a module reserves.
|
|
97
41
|
"as",
|
|
98
42
|
"await",
|
|
99
43
|
"break",
|
|
@@ -133,6 +77,31 @@ const RESERVED_DECLARATION_NAMES = new Set([
|
|
|
133
77
|
"while",
|
|
134
78
|
"with",
|
|
135
79
|
"yield",
|
|
80
|
+
// Strict mode's.
|
|
81
|
+
"implements",
|
|
82
|
+
"interface",
|
|
83
|
+
"let",
|
|
84
|
+
"package",
|
|
85
|
+
"private",
|
|
86
|
+
"protected",
|
|
87
|
+
"public",
|
|
88
|
+
"static",
|
|
89
|
+
// The predefined types.
|
|
90
|
+
"any",
|
|
91
|
+
"bigint",
|
|
92
|
+
"boolean",
|
|
93
|
+
"never",
|
|
94
|
+
"number",
|
|
95
|
+
"object",
|
|
96
|
+
"string",
|
|
97
|
+
"symbol",
|
|
98
|
+
"undefined",
|
|
99
|
+
"unknown",
|
|
100
|
+
// The type operators.
|
|
101
|
+
"infer",
|
|
102
|
+
"keyof",
|
|
103
|
+
"readonly",
|
|
104
|
+
"unique",
|
|
136
105
|
]);
|
|
137
106
|
/**
|
|
138
107
|
* **The names the emitted files declare for THEMSELVES, beside the spec's own declarations.**
|
|
@@ -150,26 +119,87 @@ const EMITTED_HELPER_NAMES = new Set([
|
|
|
150
119
|
"ByteStream",
|
|
151
120
|
"Produced",
|
|
152
121
|
"ResponseArm",
|
|
122
|
+
"SPEC_VOCABULARIES",
|
|
153
123
|
"Simplify",
|
|
154
124
|
"WireInputs",
|
|
155
125
|
"WireOutputs",
|
|
126
|
+
"WireReader",
|
|
127
|
+
"WireReaders",
|
|
156
128
|
"WireResults",
|
|
157
129
|
]);
|
|
158
130
|
/**
|
|
159
|
-
*
|
|
131
|
+
* **Library types the emitted files name bare, so a spec's type of the name takes a trailing `_`** -
|
|
132
|
+
* a multipart file's `arrayBuffer: () => globalThis.Promise<ArrayBuffer>`, a stream's `Uint8Array`
|
|
133
|
+
* chunks.
|
|
134
|
+
*
|
|
135
|
+
* **Every library GENERIC a file names is written `globalThis.Record<...>`, `globalThis.Readonly<...>`
|
|
136
|
+
* or `globalThis.Promise<...>` where it is written**, so a spec's own `model Record`, `Promise`,
|
|
137
|
+
* `Readonly` or `Exclude` is declared under its name, as openapi3 publishes it, and shadows nothing.
|
|
138
|
+
* This was a rewrite of each finished file instead - every `Record<` in a file declaring a `Record`
|
|
139
|
+
* became `globalThis.Record<` - and a file's text is not only references: the spec's own text is in
|
|
140
|
+
* it too. Measured beside `model Record`: `k?: "Record<k>"` became `z.literal("globalThis.Record<k>")`,
|
|
141
|
+
* refusing the value the document accepts and accepting one it refuses; `@pattern("^Record<...")`,
|
|
142
|
+
* a default and a `.describe()` were rewritten alike; and an enum's value `"Record<int32>"`, kept in
|
|
143
|
+
* `vocabularies.gen.ts`, was rewritten in `requests.gen.ts`, so the wire contract did not compile. Only
|
|
144
|
+
* the site writing a reference knows it is one.
|
|
145
|
+
*/
|
|
146
|
+
const LIBRARY_NAMES_WRITTEN_BARE = new Set(["ArrayBuffer", "Uint8Array"]);
|
|
147
|
+
/**
|
|
148
|
+
* The name a declaration is emitted under, from the name the document publishes it under.
|
|
160
149
|
*
|
|
161
150
|
* A trailing `_` is appended where the spec's own name cannot be a TypeScript declaration, or is one
|
|
162
151
|
* the emitted file already declares for itself. It is applied in ONE place so both walks agree:
|
|
163
152
|
* `wire-contract.gen.ts` pairs a validator against a contract type by name, and a mangling applied to
|
|
164
153
|
* one side only would break that assertion rather than the file.
|
|
154
|
+
*
|
|
155
|
+
* **Decided on the WHOLE name, visibility suffix included, and for every declaration.** It was
|
|
156
|
+
* decided on the bare name before {@link nameAt} added the suffix, so `model WireReader` seen through a
|
|
157
|
+
* PATCH, which openapi3 publishes as `WireReaderUpdate` - a name nothing in the files holds - was
|
|
158
|
+
* `WireReader_Update` and `wireReader_UpdateSchema`. And a declaration no type stands for never
|
|
159
|
+
* reached it: a multipart body `model public` was `export interface public` (`TS1214`), and a
|
|
160
|
+
* `@discriminated union Wire` with a variant `inputs` declared its envelope `WireInputs` beside the
|
|
161
|
+
* lookup of that name, and the two interfaces merged without a word.
|
|
165
162
|
*/
|
|
166
|
-
function safeDeclarationName(
|
|
167
|
-
|
|
163
|
+
function safeDeclarationName(declared) {
|
|
164
|
+
/**
|
|
165
|
+
* **A character a component key cannot hold is `_`, as openapi3 writes the key**: `model Caf\u{e9}` is
|
|
166
|
+
* published as `Caf_`, its `createValidKey`. Written as declared, the name reached identifiers -
|
|
167
|
+
* `caf\u{e9}Schema`, `interface Caf\u{e9}` - in files this package keeps to printable ASCII, under a name the
|
|
168
|
+
* document does not use. `-` and `.`, which a key may hold and an identifier may not, are `_` too.
|
|
169
|
+
*/
|
|
170
|
+
const name = declared.replace(/[^A-Za-z0-9_]/g, "_");
|
|
171
|
+
return RESERVED_DECLARATION_NAMES.has(name) ||
|
|
172
|
+
EMITTED_HELPER_NAMES.has(name) ||
|
|
173
|
+
LIBRARY_NAMES_WRITTEN_BARE.has(name)
|
|
174
|
+
? `${name}_`
|
|
175
|
+
: name;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* **The type a body is validated as: openapi3's effective payload type, at the body itself.**
|
|
179
|
+
* openapi3 resolves every request, response and part body through `getEffectivePayloadType`: what is
|
|
180
|
+
* left once metadata is set aside, matched to the model it all came from. `@body body: { ...Foo;
|
|
181
|
+
* @header h: string }` is `$ref Foo`, `Pet & {}` is `$ref Pet`; validated as written, the first
|
|
182
|
+
* required the header as a key and the second refused a subtype's own keys under sealing.
|
|
183
|
+
*/
|
|
184
|
+
function payloadTypeOf(program, type, payload, depth) {
|
|
185
|
+
if (!payload || depth !== 0 || type.kind !== "Model")
|
|
186
|
+
return type;
|
|
187
|
+
return metadataInfoOf(program).getEffectivePayloadType(type, currentVisibility());
|
|
168
188
|
}
|
|
169
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* A type earns a name when the spec gave it one - anonymous shapes stay inline where they are used.
|
|
191
|
+
* The name the document publishes it under, before any visibility suffix: {@link nameAt} makes it
|
|
192
|
+
* the name it is emitted under.
|
|
193
|
+
*/
|
|
170
194
|
function declaredNameOf(program, type) {
|
|
195
|
+
/**
|
|
196
|
+
* **`@friendlyName` names a declaration, as openapi3's `getOpenAPITypeName` reads it first.**
|
|
197
|
+
* Read as the declared name, `@friendlyName("Dog") model Cat` was validated as `Dog` by the
|
|
198
|
+
* validator named `catSchema`, and two models swapping names swapped validators.
|
|
199
|
+
*/
|
|
200
|
+
const named = type.kind === "Enum" || type.kind === "Union" ? getFriendlyName(program, type) : undefined;
|
|
171
201
|
if (type.kind === "Enum")
|
|
172
|
-
return
|
|
202
|
+
return named || type.name;
|
|
173
203
|
/**
|
|
174
204
|
* A named union earns a declaration too.
|
|
175
205
|
*
|
|
@@ -178,17 +208,26 @@ function declaredNameOf(program, type) {
|
|
|
178
208
|
* cross-field rule modelled as a discriminated union (which is how `@refine` is being replaced)
|
|
179
209
|
* would silently stop being checked at the boundary.
|
|
180
210
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
// An instance openapi3 cannot name is written out where it is used - see `isSpeakableInstance`.
|
|
212
|
+
if (type.kind === "Union") {
|
|
213
|
+
return type.name === undefined || !isSpeakableInstance(type) ? undefined : named || type.name;
|
|
214
|
+
}
|
|
183
215
|
if (type.kind !== "Model")
|
|
184
216
|
return undefined;
|
|
185
|
-
//
|
|
186
|
-
|
|
217
|
+
// A list literal is written out where it is used - see `isArrayLiteral`; `Record<T>` is an
|
|
218
|
+
// instance, and the standard library's own. A spec's model of either name is published by it.
|
|
219
|
+
if (isArrayLiteral(program, type))
|
|
220
|
+
return undefined;
|
|
221
|
+
if (type.name === "Record" && program.checker.isStdType(type))
|
|
222
|
+
return undefined;
|
|
223
|
+
/**
|
|
224
|
+
* **An anonymous model is written out where it is used**, as openapi3 writes `{ ...Pet }`: the one
|
|
225
|
+
* it names is the model a body resolves to, its effective payload type - see `payloadTypeOf` -
|
|
226
|
+
* which is that model, not the copy. Named after its source anywhere else, a property typed
|
|
227
|
+
* `{ ...Pet }` was a discriminated base, and refused with `Pet` where `Pet` was split.
|
|
228
|
+
*/
|
|
229
|
+
if (type.name === "")
|
|
187
230
|
return undefined;
|
|
188
|
-
if (type.name === "") {
|
|
189
|
-
const spread = spreadSourceName(type);
|
|
190
|
-
return spread === undefined ? undefined : safeDeclarationName(spread);
|
|
191
|
-
}
|
|
192
231
|
/**
|
|
193
232
|
* **A template INSTANTIATION carries the template's name, and it is not a declaration - unless
|
|
194
233
|
* `@friendlyName` gives it one of its own.**
|
|
@@ -211,11 +250,133 @@ function declaredNameOf(program, type) {
|
|
|
211
250
|
*/
|
|
212
251
|
if (type.templateMapper !== undefined) {
|
|
213
252
|
const friendly = getFriendlyName(program, type);
|
|
214
|
-
return friendly === undefined || friendly === "" ? undefined :
|
|
253
|
+
return friendly === undefined || friendly === "" ? undefined : friendly;
|
|
215
254
|
}
|
|
216
|
-
|
|
255
|
+
const friendly = getFriendlyName(program, type);
|
|
256
|
+
return friendly === undefined || friendly === "" ? type.name : friendly;
|
|
217
257
|
}
|
|
218
258
|
const lowerFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
|
|
259
|
+
/**
|
|
260
|
+
* `duplicate-declaration`, once per name whichever walk meets it first, saying which of the three it
|
|
261
|
+
* is: two types of one name; one model in two metadata contexts; or one model at two visibilities
|
|
262
|
+
* openapi3 names alike - PATCH at `Update` and at `Update` with `Patch`, both `XUpdate`.
|
|
263
|
+
*/
|
|
264
|
+
function reportCollision(program, name, first, second) {
|
|
265
|
+
const target = second.type ?? first.type ?? second.synthetic?.site ?? first.synthetic?.site;
|
|
266
|
+
if (target === undefined || !firstReportOf(program, "duplicate-declaration", target, name))
|
|
267
|
+
return;
|
|
268
|
+
/**
|
|
269
|
+
* **A shape no type stands for, beside another under its name**: a multipart body, whose parts
|
|
270
|
+
* are text and files, and an envelope openapi3 names after its union and variant.
|
|
271
|
+
*/
|
|
272
|
+
const synthetic = first.synthetic ?? second.synthetic;
|
|
273
|
+
if (synthetic !== undefined) {
|
|
274
|
+
reportDiagnostic(program, {
|
|
275
|
+
code: "duplicate-declaration",
|
|
276
|
+
messageId: synthetic.kind,
|
|
277
|
+
format: { name },
|
|
278
|
+
target,
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// The declaration's own model: a copy the compiler makes for a payload has no source.
|
|
283
|
+
if (first.type === second.type) {
|
|
284
|
+
// A union published in a multipart part's binary context and outside it - see `withMultipartBinary`.
|
|
285
|
+
const binary = (key) => key?.endsWith("\u0000multipart") === true;
|
|
286
|
+
if (binary(first.key) !== binary(second.key)) {
|
|
287
|
+
reportDiagnostic(program, {
|
|
288
|
+
code: "duplicate-declaration",
|
|
289
|
+
messageId: "binary",
|
|
290
|
+
format: { name },
|
|
291
|
+
target,
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const visibilities = first.at !== undefined && second.at !== undefined ? [first.at, second.at] : undefined;
|
|
296
|
+
if (visibilities === undefined || visibilities[0] === visibilities[1]) {
|
|
297
|
+
reportDiagnostic(program, {
|
|
298
|
+
code: "duplicate-declaration",
|
|
299
|
+
messageId: "context",
|
|
300
|
+
format: { name },
|
|
301
|
+
target,
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* **The request's own view is published apart**, which is the remedy where openapi3 writes one
|
|
307
|
+
* model at a request's visibility and at another: `D extends B` sent once in a create body is
|
|
308
|
+
* written at `Create` and, through `B`'s derived models, at `Read`.
|
|
309
|
+
*/
|
|
310
|
+
const view = visibilities
|
|
311
|
+
.map((each) => viewTemplateOf(each))
|
|
312
|
+
.find((template) => template !== undefined);
|
|
313
|
+
reportDiagnostic(program, {
|
|
314
|
+
code: "duplicate-declaration",
|
|
315
|
+
messageId: "visibility",
|
|
316
|
+
format: {
|
|
317
|
+
name,
|
|
318
|
+
first: visibilityName(visibilities[0] ?? Visibility.Read),
|
|
319
|
+
second: visibilityName(visibilities[1] ?? Visibility.Read),
|
|
320
|
+
remedy: view === undefined
|
|
321
|
+
? "Use the model at one of the two - for a PATCH, one 'implicitOptionality' setting - or give one use a model of its own."
|
|
322
|
+
: first.type === undefined
|
|
323
|
+
? `Send the request its own '${view}<...>' view, which openapi3 publishes apart, or use the model at one of the two - for a PATCH, one 'implicitOptionality' setting.`
|
|
324
|
+
: `Send the request its own view - ${viewRemedyOf(view, first.type)} and send it as the request's '@body' - which openapi3 publishes apart, or use the model at one of the two - for a PATCH, one 'implicitOptionality' setting.`,
|
|
325
|
+
},
|
|
326
|
+
target,
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* **Which of four things made two types one name**, each told as it is: a validator is named for
|
|
332
|
+
* its type with the first letter lowered, so `Foo` and `foo` meet only there; a visibility suffix
|
|
333
|
+
* can spell another type's name, `Thing` at `Create` beside a `ThingCreate`; a character an
|
|
334
|
+
* identifier cannot hold is `_`, so `x-y` meets `x_y`; and a namespace is dropped. Every one was
|
|
335
|
+
* told as the last, in one namespace as in two.
|
|
336
|
+
*/
|
|
337
|
+
const declaredName = (type) => type !== undefined && "name" in type && typeof type.name === "string" ? type.name : name;
|
|
338
|
+
const names = [declaredName(first.type), declaredName(second.type)];
|
|
339
|
+
if (first.typeName !== undefined &&
|
|
340
|
+
second.typeName !== undefined &&
|
|
341
|
+
first.typeName !== second.typeName) {
|
|
342
|
+
reportDiagnostic(program, {
|
|
343
|
+
code: "duplicate-declaration",
|
|
344
|
+
messageId: "cased",
|
|
345
|
+
format: { name, first: first.typeName, second: second.typeName },
|
|
346
|
+
target,
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const suffixed = [first, second].findIndex((each) => {
|
|
351
|
+
const bare = each.type === undefined ? undefined : declaredNameOf(program, each.type);
|
|
352
|
+
return bare !== undefined && safeDeclarationName(bare) !== name && each.at !== undefined;
|
|
353
|
+
});
|
|
354
|
+
if (suffixed !== -1) {
|
|
355
|
+
reportDiagnostic(program, {
|
|
356
|
+
code: "duplicate-declaration",
|
|
357
|
+
messageId: "suffixed",
|
|
358
|
+
format: {
|
|
359
|
+
name,
|
|
360
|
+
first: names[suffixed] ?? name,
|
|
361
|
+
second: names[1 - suffixed] ?? name,
|
|
362
|
+
at: visibilityName([first, second][suffixed]?.at ?? Visibility.Read),
|
|
363
|
+
},
|
|
364
|
+
target,
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Two instances of one template: openapi3 names a union instance after its template alone.
|
|
369
|
+
const template = (type) => type !== undefined && "templateMapper" in type && type.templateMapper !== undefined
|
|
370
|
+
? type.node
|
|
371
|
+
: undefined;
|
|
372
|
+
const instances = template(first.type) !== undefined && template(first.type) === template(second.type);
|
|
373
|
+
reportDiagnostic(program, {
|
|
374
|
+
code: "duplicate-declaration",
|
|
375
|
+
messageId: instances ? "instances" : names[0] !== names[1] ? "renamed" : "default",
|
|
376
|
+
format: { name, first: names[0], second: names[1] },
|
|
377
|
+
target,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
219
380
|
/**
|
|
220
381
|
* One model can need MORE THAN ONE declaration.
|
|
221
382
|
*
|
|
@@ -231,57 +392,60 @@ const lowerFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
|
|
|
231
392
|
* with the component names the document publishes.
|
|
232
393
|
*/
|
|
233
394
|
/**
|
|
234
|
-
* The visibility a type is actually DECLARED at, which is not always the one requested
|
|
395
|
+
* The visibility a type is actually DECLARED at, which is not always the one requested - see
|
|
396
|
+
* `renderedVisibilityOf`, the one rule both registries name and render by.
|
|
235
397
|
*
|
|
236
398
|
* **A type not reshaped at this visibility IS the canonical declaration, not a copy of it.** Keying
|
|
237
399
|
* on the requested visibility while only suffixing transformed types produced two cache entries
|
|
238
400
|
* sharing one identifier and an emitted file that stopped parsing:
|
|
239
401
|
* `Identifier 'approvalTierSchema' has already been declared`. An enum is the same enum in a request
|
|
240
|
-
* and a response
|
|
402
|
+
* and a response; a union holding a model the request reshapes is not, and was declared once for
|
|
403
|
+
* both - its response validating the create view - until unions were named by the same rule as
|
|
404
|
+
* models, as openapi3 names them.
|
|
241
405
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* **Extracted so both registries answer it identically.** They must: the emitted assertion pairs a
|
|
246
|
-
* validator with a contract type BY NAME, so a rule applied in one walk and paraphrased in the other
|
|
247
|
-
* is a pairing that silently compares the wrong two things.
|
|
406
|
+
* **One rule for both registries.** The emitted assertion pairs a validator with a contract type BY
|
|
407
|
+
* NAME, so a rule applied in one walk and paraphrased in the other is a pairing that silently
|
|
408
|
+
* compares the wrong two things.
|
|
248
409
|
*/
|
|
249
|
-
export function visibilityFor(program, type, requested) {
|
|
250
|
-
return type.kind === "Model" &&
|
|
251
|
-
requested !== Visibility.Read &&
|
|
252
|
-
isTransformedBy(program, type, requested)
|
|
253
|
-
? requested
|
|
254
|
-
: Visibility.Read;
|
|
255
|
-
}
|
|
256
410
|
/**
|
|
257
|
-
* A declaration's name at a visibility: the bare name at canonical, suffixed
|
|
411
|
+
* A declaration's name at a visibility: the bare name at canonical, and suffixed elsewhere only where
|
|
412
|
+
* the type is used at more than one visibility - openapi3's own rule, so a model only ever sent in a
|
|
413
|
+
* create body is `Holder` and not `HolderCreate`, as the document names it.
|
|
258
414
|
*
|
|
259
415
|
* The suffix is `@typespec/http`'s `getVisibilitySuffix`, called with `Visibility.Read` as the
|
|
260
416
|
* canonical exactly as `@typespec/openapi3` calls it in `schema-emitter.ts`. Same function, same
|
|
261
|
-
* argument, so these identifiers line up with the component names the document
|
|
262
|
-
* merely resembling them.
|
|
417
|
+
* argument, same usage rule, so these identifiers line up with the component names the document
|
|
418
|
+
* publishes rather than merely resembling them.
|
|
419
|
+
*/
|
|
420
|
+
/**
|
|
421
|
+
* **The visibility a type is rendered at, by where it is asked for.** An operation's own body,
|
|
422
|
+
* parameter or response - asked for with no body being rendered - is collapsed as openapi3 collapses
|
|
423
|
+
* a root, whatever kind of type it is; a type another refers to is reduced where openapi3 reduces a
|
|
424
|
+
* reference. They differ for a list and a union expression, which openapi3 reduces at the root and
|
|
425
|
+
* not below it.
|
|
263
426
|
*/
|
|
264
|
-
|
|
265
|
-
return
|
|
427
|
+
function visibilityAt(program, type, requested, depth) {
|
|
428
|
+
return depth === 0
|
|
429
|
+
? rootVisibilityOf(program, type, requested)
|
|
430
|
+
: renderedVisibilityOf(program, type, requested);
|
|
431
|
+
}
|
|
432
|
+
export function nameAt(bare, at, type) {
|
|
433
|
+
// Suffixed only where openapi3 suffixes it: used at more than one visibility - see `isSuffixedByUsage`.
|
|
434
|
+
const suffixed = at !== Visibility.Read && isSuffixedByUsage(type);
|
|
435
|
+
return safeDeclarationName(`${bare}${suffixed ? getVisibilitySuffix(at, Visibility.Read) : ""}`);
|
|
266
436
|
}
|
|
267
437
|
function keyFor(type, visibility) {
|
|
268
438
|
/**
|
|
269
|
-
* **Keyed on the
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* `parameters/body-optionality` emitted `export const bodyModelSchema` twice, a file that does not
|
|
274
|
-
* parse. The document has exactly one `BodyModel` component; collapsing onto the source is what
|
|
275
|
-
* makes the two artefacts agree about how many things there are.
|
|
439
|
+
* **Keyed on the named model, never a copy of it**: `@typespec/http` synthesises a fresh anonymous
|
|
440
|
+
* model per position, and two operations taking one body each hand over a copy. Both resolve to
|
|
441
|
+
* the model itself, their effective payload type - see `payloadTypeOf` - so the document's one
|
|
442
|
+
* `BodyModel` is one declaration, where two copies of it once emitted `bodyModelSchema` twice.
|
|
276
443
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (type.kind !== "Model" || type.name !== "")
|
|
283
|
-
return type;
|
|
284
|
-
return spreadSourceOf(type) ?? type;
|
|
444
|
+
// Inside an explicit `@body` a type can render differently - see `withMetadataAnnotationsIgnored`.
|
|
445
|
+
const body = metadataAnnotationsIgnored() ? "\u0000body" : "";
|
|
446
|
+
// And inside a multipart part's union holding raw `bytes` - see `withMultipartBinary`.
|
|
447
|
+
const binary = multipartBinaryContext() ? "\u0000multipart" : "";
|
|
448
|
+
return `${String(visibility)}\u0000${String(type.name ?? "")}\u0000${identityOf(type)}${body}${binary}`;
|
|
285
449
|
}
|
|
286
450
|
/** A stable per-type identity, since two models can share a name across namespaces. */
|
|
287
451
|
const identities = new WeakMap();
|
|
@@ -302,6 +466,12 @@ export class SchemaRegistry {
|
|
|
302
466
|
#inProgress = new Map();
|
|
303
467
|
/** Keys on a cycle - every one needs a structural type rather than a `z.infer` alias. */
|
|
304
468
|
#cyclic = new Set();
|
|
469
|
+
/** Identifiers refused as two shapes under one name, which no dispatch may hold. */
|
|
470
|
+
#refused = new Set();
|
|
471
|
+
/** Declarations refused as `split-visibility`, which a `@oneOf` holding them holds to nothing. */
|
|
472
|
+
#withheld = new Set();
|
|
473
|
+
/** Every type name a refused identifier stood for: `Foo` and `foo` are both `fooSchema`. */
|
|
474
|
+
#refusedTypeNames = new Set();
|
|
305
475
|
/**
|
|
306
476
|
* Inlined types currently being walked, by identity, against the number of declarations that were
|
|
307
477
|
* in flight when each was entered.
|
|
@@ -315,6 +485,8 @@ export class SchemaRegistry {
|
|
|
315
485
|
* entered since, the cycle has a name to close on and the walk may continue.
|
|
316
486
|
*/
|
|
317
487
|
#inlineInProgress = new Map();
|
|
488
|
+
/** How many bodies are being rendered - none, and the type asked for is an operation's own. */
|
|
489
|
+
#depth = 0;
|
|
318
490
|
constructor(program) {
|
|
319
491
|
this.#program = program;
|
|
320
492
|
}
|
|
@@ -323,8 +495,20 @@ export class SchemaRegistry {
|
|
|
323
495
|
* inline source. Declaring is depth-first so a model's dependencies are emitted above it, which is
|
|
324
496
|
* what lets the generated file be plain `const` declarations with no forward references.
|
|
325
497
|
*/
|
|
326
|
-
expressionFor(
|
|
498
|
+
expressionFor(given, payload = false) {
|
|
499
|
+
const type = payloadTypeOf(this.#program, given, payload, this.#depth);
|
|
500
|
+
// Published as a reference to another document, with no schema to validate.
|
|
501
|
+
const external = externalReferenceOf(type);
|
|
502
|
+
if (external !== undefined)
|
|
503
|
+
return refuseExternalReference(this.#program, type, external);
|
|
327
504
|
const bare = declaredNameOf(this.#program, type);
|
|
505
|
+
const requested = currentVisibility();
|
|
506
|
+
/**
|
|
507
|
+
* Asked of the model a declaration stands for, as openapi3 asks it of the effective payload type:
|
|
508
|
+
* a spread body's copy holds only what the request sends, so it is reshaped by nothing, while the
|
|
509
|
+
* model it copies - its `@path` property payload at `Read` - is.
|
|
510
|
+
*/
|
|
511
|
+
const at = visibilityAt(this.#program, type, requested, this.#depth);
|
|
328
512
|
if (bare === undefined) {
|
|
329
513
|
/**
|
|
330
514
|
* **A back edge to an INLINED type is a cycle with no name to close it**, and following it
|
|
@@ -342,7 +526,7 @@ export class SchemaRegistry {
|
|
|
342
526
|
}
|
|
343
527
|
this.#inlineInProgress.set(type, this.#inProgress.size);
|
|
344
528
|
try {
|
|
345
|
-
return this.#inline(type);
|
|
529
|
+
return this.#inline(type, at, undefined);
|
|
346
530
|
}
|
|
347
531
|
finally {
|
|
348
532
|
this.#inlineInProgress.delete(type);
|
|
@@ -357,7 +541,6 @@ export class SchemaRegistry {
|
|
|
357
541
|
* request and a response. Collapsing onto `Read` first is what makes the key and the name agree,
|
|
358
542
|
* and it is the same collapse openapi3 performs before naming a component.
|
|
359
543
|
*/
|
|
360
|
-
const at = visibilityFor(this.#program, type, currentVisibility());
|
|
361
544
|
const key = keyFor(type, at);
|
|
362
545
|
const existing = this.#declarations.get(key);
|
|
363
546
|
if (existing !== undefined)
|
|
@@ -383,34 +566,20 @@ export class SchemaRegistry {
|
|
|
383
566
|
this.#cyclic.add(inFlight);
|
|
384
567
|
return pending;
|
|
385
568
|
}
|
|
386
|
-
const name = nameAt(bare, at);
|
|
569
|
+
const name = nameAt(bare, at, type);
|
|
387
570
|
const identifier = `${lowerFirst(name)}Schema`;
|
|
388
|
-
this.#
|
|
389
|
-
const rendered = captureBackEdges(() => this.#inline(type));
|
|
390
|
-
this.#inProgress.delete(key);
|
|
391
|
-
/**
|
|
392
|
-
* A back edge that survived the whole body reached this declaration by a path with no object
|
|
393
|
-
* property on it - a named union variant, or a dictionary value - so nothing could make it lazy
|
|
394
|
-
* and the emitted `const` would read itself while initialising. `z.lazy()` defers the whole body
|
|
395
|
-
* instead, which is the only place left to put the deferral once no property can hold it.
|
|
396
|
-
*/
|
|
397
|
-
/**
|
|
398
|
-
* **The description goes on the declaration's own body, so the exported const IS the
|
|
399
|
-
* described schema.** Every reference then names that one instance, which is what keeps
|
|
400
|
-
* identity, `z.compile`'s clone (it inherits the description through its parent link) and
|
|
401
|
-
* Zod's `$ref` extraction all intact. Described at a reference instead, each use would be a
|
|
402
|
-
* fresh clone. Read from the canonical type, because an anonymous payload has its doc only on
|
|
403
|
-
* the model it was spread from, and a visibility projection carries the doc of the model it
|
|
404
|
-
* projects - as openapi3 publishes one component for both.
|
|
405
|
-
*/
|
|
406
|
-
const body = rendered.deferred ? `z.lazy(() => ${rendered.value})` : rendered.value;
|
|
407
|
-
const source = described(body, getDoc(this.#program, canonicalTypeFor(type)));
|
|
571
|
+
const { source, deferred, consulted } = this.#render(type, at, name, key, identifier);
|
|
408
572
|
const declaration = {
|
|
409
573
|
identifier,
|
|
410
574
|
typeName: name,
|
|
411
575
|
source,
|
|
412
|
-
|
|
413
|
-
|
|
576
|
+
type,
|
|
577
|
+
at,
|
|
578
|
+
key,
|
|
579
|
+
ignore: metadataAnnotationsIgnored(),
|
|
580
|
+
...(this.#cyclic.has(key) ? { structural: this.#structuralBodyOf(type, at) } : {}),
|
|
581
|
+
...(deferred ? { annotated: true } : {}),
|
|
582
|
+
...(consulted.size === 0 ? {} : { consulted }),
|
|
414
583
|
};
|
|
415
584
|
/**
|
|
416
585
|
* **Two different types can resolve to one identifier, and the module then does not parse.**
|
|
@@ -429,19 +598,191 @@ export class SchemaRegistry {
|
|
|
429
598
|
* `duplicate-id`; neither resolves it for you, because only the author knows which name to
|
|
430
599
|
* change.
|
|
431
600
|
*/
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
601
|
+
const twin = this.#order.find((candidate) => candidate.identifier === identifier);
|
|
602
|
+
/**
|
|
603
|
+
* **One declaration reached in two contexts that render it alike is one declaration.** A type
|
|
604
|
+
* under an explicit `@body` is keyed apart from the same type elsewhere, since the body can
|
|
605
|
+
* publish a `@header` as data; where nothing in it does, the two are the same text and the
|
|
606
|
+
* document's one component.
|
|
607
|
+
*/
|
|
608
|
+
if (twin !== undefined && twin.source === declaration.source) {
|
|
609
|
+
this.#declarations.set(key, twin);
|
|
610
|
+
return twin.identifier;
|
|
611
|
+
}
|
|
612
|
+
if (twin !== undefined) {
|
|
613
|
+
reportCollision(this.#program, name, twin, declaration);
|
|
614
|
+
/**
|
|
615
|
+
* **One name, one declaration, and it validates nothing.** Two differing shapes under one
|
|
616
|
+
* identifier was a module declaring it twice, which does not parse. The document holds one
|
|
617
|
+
* of them under the name and openapi3 refuses its own document for it, so neither is kept.
|
|
618
|
+
*/
|
|
619
|
+
const refused = {
|
|
620
|
+
identifier,
|
|
621
|
+
typeName: name,
|
|
622
|
+
source: UNREPRESENTABLE,
|
|
623
|
+
...(twin.type === undefined ? {} : { type: twin.type }),
|
|
624
|
+
...(twin.at === undefined ? {} : { at: twin.at }),
|
|
625
|
+
};
|
|
626
|
+
this.#order[this.#order.indexOf(twin)] = refused;
|
|
627
|
+
for (const [other, value] of this.#declarations) {
|
|
628
|
+
if (value === twin)
|
|
629
|
+
this.#declarations.set(other, refused);
|
|
630
|
+
}
|
|
631
|
+
this.#declarations.set(key, refused);
|
|
632
|
+
this.#refused.add(identifier);
|
|
633
|
+
this.#refusedTypeNames.add(twin.typeName).add(name);
|
|
634
|
+
if (twin.type !== undefined)
|
|
635
|
+
this.#refuseEnvelopesOf(twin.type);
|
|
636
|
+
this.#renderDispatchesHolding(identifier);
|
|
637
|
+
return identifier;
|
|
439
638
|
}
|
|
440
639
|
this.#declarations.set(key, declaration);
|
|
441
640
|
this.#order.push(declaration);
|
|
442
641
|
this.#declareRelatives(type);
|
|
443
642
|
return declaration.identifier;
|
|
444
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* A declaration's body, rendered with its key in flight so a reference back to it is a back edge.
|
|
646
|
+
*
|
|
647
|
+
* A back edge that survived the whole body reached this declaration by a path with no object
|
|
648
|
+
* property on it - a named union variant, or a dictionary value - so nothing could make it lazy
|
|
649
|
+
* and the emitted `const` would read itself while initialising. `z.lazy()` defers the whole body
|
|
650
|
+
* instead, which is the only place left to put the deferral once no property can hold it.
|
|
651
|
+
*/
|
|
652
|
+
#render(type, at, name, key, identifier) {
|
|
653
|
+
this.#inProgress.set(key, identifier);
|
|
654
|
+
const consulted = new Set();
|
|
655
|
+
this.#consulted.push(consulted);
|
|
656
|
+
const rendered = (() => {
|
|
657
|
+
try {
|
|
658
|
+
return captureBackEdges(() => this.#inline(type, at, name));
|
|
659
|
+
}
|
|
660
|
+
finally {
|
|
661
|
+
this.#consulted.pop();
|
|
662
|
+
}
|
|
663
|
+
})();
|
|
664
|
+
this.#inProgress.delete(key);
|
|
665
|
+
if (rendered.value === UNREPRESENTABLE &&
|
|
666
|
+
isSplitByVisibility(this.#program, type, at, metadataAnnotationsIgnored(), name)) {
|
|
667
|
+
this.#withheld.add(identifier);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* **The description goes on the declaration's own body, so the exported const IS the
|
|
671
|
+
* described schema.** Every reference then names that one instance, which is what keeps
|
|
672
|
+
* identity, `z.compile`'s clone (it inherits the description through its parent link) and
|
|
673
|
+
* Zod's `$ref` extraction all intact. Described at a reference instead, each use would be a
|
|
674
|
+
* fresh clone. A visibility projection carries the doc of the model it projects, as openapi3
|
|
675
|
+
* publishes one component for both.
|
|
676
|
+
*/
|
|
677
|
+
const body = rendered.deferred ? `z.lazy(() => ${rendered.value})` : rendered.value;
|
|
678
|
+
const source = described(body, getDoc(this.#program, type));
|
|
679
|
+
return { source, deferred: rendered.deferred, consulted };
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* **Every dispatch rendered before a subtype it holds was refused, rendered again without it.** A
|
|
683
|
+
* refusal is found where the second shape under one name is declared, and a base's dispatch can
|
|
684
|
+
* already hold the first: `z.discriminatedUnion` given the `z.never()` the refusal leaves throws
|
|
685
|
+
* on every parse, and the module does not typecheck. Each is rendered in its own context, its key
|
|
686
|
+
* in flight, so a reference back to it is still a back edge.
|
|
687
|
+
*/
|
|
688
|
+
/**
|
|
689
|
+
* **A union refused takes its envelopes with it.** openapi3 publishes each envelope wherever it
|
|
690
|
+
* publishes the union, under one name: where the union is two schemas under its one name, each
|
|
691
|
+
* envelope is too, and the document keeps the one written last - `DU7Y` with its `kind` and
|
|
692
|
+
* `value` optional, written from a PATCH - while its validator held the view it was first declared
|
|
693
|
+
* at, requiring both. Refused with the union, whose diagnostic says why; the contract types follow.
|
|
694
|
+
*/
|
|
695
|
+
#refuseEnvelopesOf(union) {
|
|
696
|
+
for (const [index, declaration] of this.#order.entries()) {
|
|
697
|
+
if (declaration.synthetic?.kind !== "envelope" || declaration.synthetic.site !== union)
|
|
698
|
+
continue;
|
|
699
|
+
if (declaration.source === UNREPRESENTABLE)
|
|
700
|
+
continue;
|
|
701
|
+
const refused = { ...declaration, source: UNREPRESENTABLE };
|
|
702
|
+
this.#order[index] = refused;
|
|
703
|
+
for (const [other, value] of this.#declarations) {
|
|
704
|
+
if (value === declaration)
|
|
705
|
+
this.#declarations.set(other, refused);
|
|
706
|
+
}
|
|
707
|
+
this.#refused.add(declaration.identifier);
|
|
708
|
+
this.#refusedTypeNames.add(declaration.typeName);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** What a value rendered here may ask of the declarations it refers to - see `HeldDeclarations`. */
|
|
712
|
+
#held = {
|
|
713
|
+
refused: (expression) => this.#refused.has(expression),
|
|
714
|
+
annotated: (expression) => this.#order.some((declaration) => declaration.identifier === expression && declaration.annotated === true),
|
|
715
|
+
refusedAt: (type, at) => {
|
|
716
|
+
const key = keyFor(type, at);
|
|
717
|
+
this.#consulted.at(-1)?.add(key);
|
|
718
|
+
const declared = this.#declarations.get(key);
|
|
719
|
+
return (declared !== undefined &&
|
|
720
|
+
(this.#refused.has(declared.identifier) || this.#withheld.has(declared.identifier)));
|
|
721
|
+
},
|
|
722
|
+
holdsRefusalAt: (type, at) => {
|
|
723
|
+
const key = keyFor(type, at);
|
|
724
|
+
this.#consulted.at(-1)?.add(key);
|
|
725
|
+
const declared = this.#declarations.get(key);
|
|
726
|
+
return declared !== undefined && this.#reachesRefusal(declared.identifier, new Set());
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
/** Whether a declaration is refused, or its source refers to one that reaches a refusal. */
|
|
730
|
+
#reachesRefusal(identifier, seen) {
|
|
731
|
+
if (seen.has(identifier))
|
|
732
|
+
return false;
|
|
733
|
+
seen.add(identifier);
|
|
734
|
+
if (this.#refused.has(identifier) || this.#withheld.has(identifier))
|
|
735
|
+
return true;
|
|
736
|
+
const declaration = this.#order.find((each) => each.identifier === identifier);
|
|
737
|
+
if (declaration === undefined)
|
|
738
|
+
return false;
|
|
739
|
+
return [...declaration.source.matchAll(/[A-Za-z_$][\w$]*/g)].some(([name]) => name !== identifier && this.#declares(name) && this.#reachesRefusal(name, seen));
|
|
740
|
+
}
|
|
741
|
+
#declares(identifier) {
|
|
742
|
+
return this.#order.some((each) => each.identifier === identifier);
|
|
743
|
+
}
|
|
744
|
+
/** The keys each declaration in flight asked `refusedAt` of, innermost last. */
|
|
745
|
+
#consulted = [];
|
|
746
|
+
/** What the contract types' walk asks of these declarations, so a merge is `never` on both sides. */
|
|
747
|
+
get held() {
|
|
748
|
+
return this.#held;
|
|
749
|
+
}
|
|
750
|
+
#renderDispatchesHolding(identifier) {
|
|
751
|
+
const holds = new RegExp(`\\b${identifier}\\b`);
|
|
752
|
+
const refusedKeys = new Set([...this.#declarations]
|
|
753
|
+
.filter(([, value]) => value.identifier === identifier)
|
|
754
|
+
.map(([each]) => each));
|
|
755
|
+
// By index: rendering one again can declare more, which hold no refused subtype.
|
|
756
|
+
for (let index = 0; index < this.#order.length; index += 1) {
|
|
757
|
+
const held = this.#order[index];
|
|
758
|
+
const { type, at, key } = held;
|
|
759
|
+
if (type === undefined || at === undefined || key === undefined)
|
|
760
|
+
continue;
|
|
761
|
+
// A dispatch, or a `@oneOf` that now holds a refused member - see `holdsRefusedMember` -
|
|
762
|
+
// directly or through the declarations it refers to.
|
|
763
|
+
const dispatches = (held.source.includes("z.discriminatedUnion(") || held.source.includes("z.xor(")) &&
|
|
764
|
+
(holds.test(held.source) || this.#reachesRefusal(held.identifier, new Set()));
|
|
765
|
+
// A merge that asked before the refusal holds the shape it now refuses - see `refusedAt`.
|
|
766
|
+
const merges = [...(held.consulted ?? [])].some((each) => refusedKeys.has(each));
|
|
767
|
+
if (!dispatches && !merges)
|
|
768
|
+
continue;
|
|
769
|
+
const keys = [...this.#declarations]
|
|
770
|
+
.filter(([, value]) => value === held)
|
|
771
|
+
.map(([each]) => each);
|
|
772
|
+
for (const each of keys)
|
|
773
|
+
this.#declarations.delete(each);
|
|
774
|
+
const { source, deferred, consulted } = withMetadataAnnotationsIgnored(held.ignore ?? false, () => this.#render(type, at, held.typeName, key, held.identifier));
|
|
775
|
+
const fresh = {
|
|
776
|
+
...held,
|
|
777
|
+
source,
|
|
778
|
+
...(deferred ? { annotated: true } : {}),
|
|
779
|
+
consulted,
|
|
780
|
+
};
|
|
781
|
+
this.#order[index] = fresh;
|
|
782
|
+
for (const each of keys)
|
|
783
|
+
this.#declarations.set(each, fresh);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
445
786
|
/**
|
|
446
787
|
* Declare the BASE CHAIN a component's own declaration implies.
|
|
447
788
|
*
|
|
@@ -451,8 +792,8 @@ export class SchemaRegistry {
|
|
|
451
792
|
* them.
|
|
452
793
|
*
|
|
453
794
|
* **Subtypes are deliberately NOT walked here, because nothing would be walking them.** A
|
|
454
|
-
* discriminated base renders as a dispatch over its subtypes - `z.discriminatedUnion`, beside
|
|
455
|
-
*
|
|
795
|
+
* discriminated base renders as a dispatch over its subtypes - `z.discriminatedUnion`, beside the base
|
|
796
|
+
* itself - and rendering that already asks the registry for each one. A loop here looked load-bearing and was measured not to
|
|
456
797
|
* be: removing it left all 23 polymorphism and differential arms green.
|
|
457
798
|
*
|
|
458
799
|
* **Wrapped in {@link captureBackEdges} and the result discarded.** This call can land on a
|
|
@@ -466,14 +807,35 @@ export class SchemaRegistry {
|
|
|
466
807
|
const base = type.baseModel;
|
|
467
808
|
captureBackEdges(() => this.expressionFor(base));
|
|
468
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Declare a component the document publishes, at the visibility the walk is at, as a type
|
|
812
|
+
* another refers to - see `publishedComponents`. Nothing refers to it here, so a declaration
|
|
813
|
+
* still in flight is no back edge.
|
|
814
|
+
*/
|
|
815
|
+
declare(type) {
|
|
816
|
+
this.#depth += 1;
|
|
817
|
+
try {
|
|
818
|
+
captureBackEdges(() => this.expressionFor(type));
|
|
819
|
+
}
|
|
820
|
+
finally {
|
|
821
|
+
this.#depth -= 1;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
469
824
|
/**
|
|
470
825
|
* The Zod for a single property, constraints and optionality included.
|
|
471
826
|
*
|
|
472
827
|
* Used for path/query parameters, which are `ModelProperty`s that belong to no model - so they
|
|
473
828
|
* need the property-level walk rather than the type-level one, or `@minLength` on a path param
|
|
474
829
|
* would be documented and never enforced.
|
|
830
|
+
*
|
|
831
|
+
* **The property is where a refusal inside it points**, as a model's member is - see
|
|
832
|
+
* `withDeclarationSite`. A parameter is walked outside any model, so `@query a?: never[]` was
|
|
833
|
+
* reported at `<unknown location>:1:1`, the intrinsic's own place, which names nothing to edit.
|
|
475
834
|
*/
|
|
476
|
-
expressionForProperty(property) {
|
|
835
|
+
expressionForProperty(property, position = OWN_POSITION) {
|
|
836
|
+
return withParameterPosition(true, () => withDeclarationSite(property, () => this.#parameter(property, position)));
|
|
837
|
+
}
|
|
838
|
+
#parameter(property, position) {
|
|
477
839
|
return withRefResolver(
|
|
478
840
|
/**
|
|
479
841
|
* **Every candidate goes through {@link expressionFor}, including the ones with no name.**
|
|
@@ -485,7 +847,7 @@ export class SchemaRegistry {
|
|
|
485
847
|
* `expressionFor` renders an undeclared type through `#inline` exactly as before, and now
|
|
486
848
|
* also notices when it is already doing so.
|
|
487
849
|
*/
|
|
488
|
-
(candidate) => this.expressionFor(candidate), () => propertyToZod(this.#program, property));
|
|
850
|
+
(candidate) => this.expressionFor(candidate), () => withHeldDeclarations(this.#held, () => propertyToZod(this.#program, property, position)));
|
|
489
851
|
}
|
|
490
852
|
/**
|
|
491
853
|
* A declaration for a shape the spec never wrote, under a name the document publishes.
|
|
@@ -499,10 +861,12 @@ export class SchemaRegistry {
|
|
|
499
861
|
* body: the document publishes `MultiPartRequest` as a component, and there is no walkable type
|
|
500
862
|
* for it - the parts have to be assembled from `@typespec/http`'s own view of the payload.
|
|
501
863
|
*/
|
|
502
|
-
declareNamed(name, build) {
|
|
503
|
-
return this.#declareSynthetic(name, build);
|
|
864
|
+
declareNamed(name, build, site) {
|
|
865
|
+
return this.#declareSynthetic(name, build, { kind: "multipart", site });
|
|
504
866
|
}
|
|
505
|
-
#declareSynthetic(
|
|
867
|
+
#declareSynthetic(published, build, synthetic) {
|
|
868
|
+
// Under the rule every declaration's name is decided by - see `safeDeclarationName`.
|
|
869
|
+
const name = safeDeclarationName(published);
|
|
506
870
|
const key = `synthetic\u0000${name}`;
|
|
507
871
|
const existing = this.#declarations.get(key);
|
|
508
872
|
if (existing !== undefined)
|
|
@@ -513,7 +877,31 @@ export class SchemaRegistry {
|
|
|
513
877
|
this.#inProgress.set(key, identifier);
|
|
514
878
|
const source = build();
|
|
515
879
|
this.#inProgress.delete(key);
|
|
516
|
-
const declaration = { identifier, typeName: name, source };
|
|
880
|
+
const declaration = { identifier, typeName: name, source, synthetic };
|
|
881
|
+
/**
|
|
882
|
+
* **A name a type already declares is the same collision as any other**: a model sent as JSON
|
|
883
|
+
* and as a multipart form, its JSON component declared first, declared `formSchema` twice, and
|
|
884
|
+
* the module did not parse. One rendered alike is the one declaration.
|
|
885
|
+
*/
|
|
886
|
+
const twin = this.#order.find((candidate) => candidate.identifier === identifier);
|
|
887
|
+
if (twin !== undefined && twin.source === source) {
|
|
888
|
+
this.#declarations.set(key, twin);
|
|
889
|
+
return identifier;
|
|
890
|
+
}
|
|
891
|
+
if (twin !== undefined) {
|
|
892
|
+
reportCollision(this.#program, name, twin, declaration);
|
|
893
|
+
const refused = { ...twin, source: UNREPRESENTABLE };
|
|
894
|
+
this.#order[this.#order.indexOf(twin)] = refused;
|
|
895
|
+
for (const [other, value] of this.#declarations) {
|
|
896
|
+
if (value === twin)
|
|
897
|
+
this.#declarations.set(other, refused);
|
|
898
|
+
}
|
|
899
|
+
this.#declarations.set(key, refused);
|
|
900
|
+
this.#refused.add(identifier);
|
|
901
|
+
this.#refusedTypeNames.add(twin.typeName).add(name);
|
|
902
|
+
this.#renderDispatchesHolding(identifier);
|
|
903
|
+
return identifier;
|
|
904
|
+
}
|
|
517
905
|
this.#declarations.set(key, declaration);
|
|
518
906
|
this.#order.push(declaration);
|
|
519
907
|
return identifier;
|
|
@@ -532,8 +920,27 @@ export class SchemaRegistry {
|
|
|
532
920
|
* `wire-contract.gen.ts`, and a third spelling of "what this shape is" would be a third thing to
|
|
533
921
|
* drift.
|
|
534
922
|
*/
|
|
535
|
-
#structuralBodyOf(type) {
|
|
536
|
-
|
|
923
|
+
#structuralBodyOf(type, at) {
|
|
924
|
+
/**
|
|
925
|
+
* At the visibility the declaration is keyed on, as its body was rendered: read at the one it
|
|
926
|
+
* was requested at, a discriminated base reached from a create body annotated itself with
|
|
927
|
+
* `DogCreate`, a declaration nothing made.
|
|
928
|
+
*/
|
|
929
|
+
return withVisibility(at, () => withParameterPosition(false, () => withTsRefResolver((candidate) => this.#typeNameOf(candidate) ?? this.#reducedBodyOf(candidate), () => typeToTsBody(this.#program, type))));
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* **A type with no declaration of its own is written out where openapi3 reduces it to**, as
|
|
933
|
+
* {@link TypeRegistry.expressionFor} writes it: `q?: Record<M4>` under a PATCH is a `Record`
|
|
934
|
+
* openapi3 renders at `Read`, its value `M4`. Walked at the PATCH, the annotation named
|
|
935
|
+
* `M4Update`, which nothing declares (`TS2552`). `undefined` where the visibility stands, so the
|
|
936
|
+
* walk goes on as it was.
|
|
937
|
+
*/
|
|
938
|
+
#reducedBodyOf(candidate) {
|
|
939
|
+
const requested = currentVisibility();
|
|
940
|
+
const at = renderedVisibilityOf(this.#program, candidate, requested);
|
|
941
|
+
return at === requested
|
|
942
|
+
? undefined
|
|
943
|
+
: withVisibility(at, () => typeToTsBody(this.#program, candidate));
|
|
537
944
|
}
|
|
538
945
|
/**
|
|
539
946
|
* **The TypeScript name a reference resolves to is the one {@link expressionFor} declares it
|
|
@@ -546,7 +953,7 @@ export class SchemaRegistry {
|
|
|
546
953
|
const bare = declaredNameOf(this.#program, candidate);
|
|
547
954
|
if (bare === undefined)
|
|
548
955
|
return undefined;
|
|
549
|
-
return nameAt(bare,
|
|
956
|
+
return nameAt(bare, renderedVisibilityOf(this.#program, candidate, currentVisibility()), candidate);
|
|
550
957
|
}
|
|
551
958
|
/**
|
|
552
959
|
* Render `type` without consulting its own declaration, but letting nested types use theirs.
|
|
@@ -558,8 +965,28 @@ export class SchemaRegistry {
|
|
|
558
965
|
* `RangeError: Maximum call stack size exceeded`. Skipping the resolver once, here, is what makes
|
|
559
966
|
* a nested occurrence resolve to the identifier while the body still renders as a body.
|
|
560
967
|
*/
|
|
561
|
-
#inline(type) {
|
|
562
|
-
|
|
968
|
+
#inline(type, at, declared) {
|
|
969
|
+
// What openapi3 publishes with its properties read at another visibility - see `published.ts`.
|
|
970
|
+
const ignore = metadataAnnotationsIgnored();
|
|
971
|
+
if (isSplitByVisibility(this.#program, type, at, ignore, declared))
|
|
972
|
+
return UNREPRESENTABLE;
|
|
973
|
+
this.#depth += 1;
|
|
974
|
+
try {
|
|
975
|
+
// A declaration is a component every use shares, a parameter's included.
|
|
976
|
+
return declared === undefined
|
|
977
|
+
? this.#body(type, at)
|
|
978
|
+
: withParameterPosition(false, () => this.#body(type, at));
|
|
979
|
+
}
|
|
980
|
+
finally {
|
|
981
|
+
this.#depth -= 1;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/** A model's body renders its own properties, outside a part's binary union - see `withMultipartBinary`. */
|
|
985
|
+
#body(type, at) {
|
|
986
|
+
return withMultipartBinary(multipartBinaryContext() && type.kind !== "Model", () => this.#bodyAt(type, at));
|
|
987
|
+
}
|
|
988
|
+
#bodyAt(type, at) {
|
|
989
|
+
return withVisibility(at, () => withRefResolver(
|
|
563
990
|
/**
|
|
564
991
|
* **Every candidate goes through {@link expressionFor}, including the ones with no name.**
|
|
565
992
|
*
|
|
@@ -570,7 +997,7 @@ export class SchemaRegistry {
|
|
|
570
997
|
* `expressionFor` renders an undeclared type through `#inline` exactly as before, and now
|
|
571
998
|
* also notices when it is already doing so.
|
|
572
999
|
*/
|
|
573
|
-
(candidate) => this.expressionFor(candidate), () => withDeferredAnnotator((property) => this.#deferredAnnotationOf(property), () => withSyntheticDeclarations((name, build) => this.#declareSynthetic(name, build), () => typeToZodBody(this.#program, type))));
|
|
1000
|
+
(candidate) => this.expressionFor(candidate), () => withDeferredAnnotator((property, position) => this.#deferredAnnotationOf(property, position), (variant) => this.#deferredTypeAnnotationOf(variant), () => withSyntheticDeclarations((name, build, site) => this.#declareSynthetic(name, build, { kind: "envelope", site }), () => withHeldDeclarations(this.#held, () => typeToZodBody(this.#program, type))))));
|
|
574
1001
|
}
|
|
575
1002
|
/**
|
|
576
1003
|
* The Zod type a property emitted as a getter is annotated with, or `undefined` for none.
|
|
@@ -587,18 +1014,32 @@ export class SchemaRegistry {
|
|
|
587
1014
|
* getter that its own value does not satisfy. A recursive property carrying a default is the one
|
|
588
1015
|
* shape this leaves as it was.
|
|
589
1016
|
*/
|
|
590
|
-
#deferredAnnotationOf(property) {
|
|
1017
|
+
#deferredAnnotationOf(property, position) {
|
|
591
1018
|
if (property.defaultValue !== undefined)
|
|
592
1019
|
return undefined;
|
|
593
|
-
const declared = withTsRefResolver((candidate) => this.#typeNameOf(candidate), () => propertyTypeToTs(this.#program, property));
|
|
1020
|
+
const declared = withTsRefResolver((candidate) => this.#typeNameOf(candidate), () => propertyTypeToTs(this.#program, property, position));
|
|
594
1021
|
const core = `z.ZodType<${declared}, ${declared}>`;
|
|
595
|
-
const optional = property.optional || isOptionalAt(this.#program, property);
|
|
1022
|
+
const optional = !position.required && (property.optional || isOptionalAt(this.#program, property));
|
|
596
1023
|
return `: ${optional ? `z.ZodExactOptional<${core}>` : core}`;
|
|
597
1024
|
}
|
|
1025
|
+
/** The annotation for a deferred value no property declares - an envelope's variant. */
|
|
1026
|
+
#deferredTypeAnnotationOf(type) {
|
|
1027
|
+
const declared = withTsRefResolver((candidate) => this.#typeNameOf(candidate), () => typeToTs(this.#program, type));
|
|
1028
|
+
return `: z.ZodType<${declared}, ${declared}>`;
|
|
1029
|
+
}
|
|
598
1030
|
/** Every declaration, dependencies first. */
|
|
599
1031
|
declarations() {
|
|
600
1032
|
return this.#order;
|
|
601
1033
|
}
|
|
1034
|
+
/** The names of the declarations that validate nothing, refused here or in the walk beneath. */
|
|
1035
|
+
refusedNames() {
|
|
1036
|
+
return new Set([
|
|
1037
|
+
...this.#refusedTypeNames,
|
|
1038
|
+
...this.#order
|
|
1039
|
+
.filter((declaration) => declaration.source === UNREPRESENTABLE)
|
|
1040
|
+
.map((declaration) => declaration.typeName),
|
|
1041
|
+
]);
|
|
1042
|
+
}
|
|
602
1043
|
}
|
|
603
1044
|
/**
|
|
604
1045
|
* The same walk as {@link SchemaRegistry}, emitting plain TypeScript instead of Zod.
|
|
@@ -622,7 +1063,7 @@ export class SchemaRegistry {
|
|
|
622
1063
|
* whole body is a single `{...}`, whatever it contains. A composition operator cannot be missed,
|
|
623
1064
|
* because anything following the matching close brace fails the test whatever it happens to be.
|
|
624
1065
|
*/
|
|
625
|
-
function isSingleObjectLiteral(source) {
|
|
1066
|
+
export function isSingleObjectLiteral(source) {
|
|
626
1067
|
if (!source.startsWith("{"))
|
|
627
1068
|
return false;
|
|
628
1069
|
let depth = 0;
|
|
@@ -661,7 +1102,7 @@ export class TypeRegistry {
|
|
|
661
1102
|
* projection. The two registries pair BY NAME in the emitted assertion, so they have to agree
|
|
662
1103
|
* about how many declarations there are before the pairing means anything.
|
|
663
1104
|
*
|
|
664
|
-
* Both now go through the shared
|
|
1105
|
+
* Both now go through the shared `renderedVisibilityOf` and {@link nameAt}: paraphrasing either
|
|
665
1106
|
* rule in one walk is how a validator ends up compared against a type describing something else.
|
|
666
1107
|
*/
|
|
667
1108
|
#declarations = new Map();
|
|
@@ -675,26 +1116,46 @@ export class TypeRegistry {
|
|
|
675
1116
|
* Reported only by the schema walk, so one spec does not produce the same complaint twice.
|
|
676
1117
|
*/
|
|
677
1118
|
#inlineInProgress = new Map();
|
|
678
|
-
|
|
1119
|
+
/** How many bodies are being rendered - see the schema registry's. */
|
|
1120
|
+
#depth = 0;
|
|
1121
|
+
/** The validators' declarations, whose refusals a merge here holds to as theirs do. */
|
|
1122
|
+
#held;
|
|
1123
|
+
constructor(program, held) {
|
|
679
1124
|
this.#program = program;
|
|
1125
|
+
this.#held = held ?? {
|
|
1126
|
+
refused: () => false,
|
|
1127
|
+
annotated: () => false,
|
|
1128
|
+
refusedAt: () => false,
|
|
1129
|
+
holdsRefusalAt: () => false,
|
|
1130
|
+
};
|
|
680
1131
|
}
|
|
681
|
-
expressionFor(
|
|
1132
|
+
expressionFor(given, payload = false) {
|
|
1133
|
+
const type = payloadTypeOf(this.#program, given, payload, this.#depth);
|
|
1134
|
+
// Refused by the schema walk, which reports it - see `externalReferenceOf`.
|
|
1135
|
+
if (externalReferenceOf(type) !== undefined)
|
|
1136
|
+
return "never";
|
|
682
1137
|
const bare = declaredNameOf(this.#program, type);
|
|
1138
|
+
const requested = currentVisibility();
|
|
1139
|
+
/**
|
|
1140
|
+
* Asked of the model a declaration stands for, as openapi3 asks it of the effective payload type:
|
|
1141
|
+
* a spread body's copy holds only what the request sends, so it is reshaped by nothing, while the
|
|
1142
|
+
* model it copies - its `@path` property payload at `Read` - is.
|
|
1143
|
+
*/
|
|
1144
|
+
const at = visibilityAt(this.#program, type, requested, this.#depth);
|
|
683
1145
|
if (bare === undefined) {
|
|
684
1146
|
const entered = this.#inlineInProgress.get(type);
|
|
685
1147
|
if (entered !== undefined && this.#inProgress.size <= entered)
|
|
686
1148
|
return "never";
|
|
687
1149
|
this.#inlineInProgress.set(type, this.#inProgress.size);
|
|
688
1150
|
try {
|
|
689
|
-
return this.#inline(type);
|
|
1151
|
+
return this.#inline(type, at, undefined);
|
|
690
1152
|
}
|
|
691
1153
|
finally {
|
|
692
1154
|
this.#inlineInProgress.delete(type);
|
|
693
1155
|
}
|
|
694
1156
|
}
|
|
695
|
-
const at = visibilityFor(this.#program, type, currentVisibility());
|
|
696
1157
|
const key = keyFor(type, at);
|
|
697
|
-
const name = nameAt(bare, at);
|
|
1158
|
+
const name = nameAt(bare, at, type);
|
|
698
1159
|
const existing = this.#declarations.get(key);
|
|
699
1160
|
if (existing !== undefined)
|
|
700
1161
|
return existing.name;
|
|
@@ -712,10 +1173,11 @@ export class TypeRegistry {
|
|
|
712
1173
|
if (pending !== undefined)
|
|
713
1174
|
return pending;
|
|
714
1175
|
this.#inProgress.set(key, name);
|
|
715
|
-
const source = this.#inline(type);
|
|
1176
|
+
const source = this.#inline(type, at, name);
|
|
716
1177
|
this.#inProgress.delete(key);
|
|
717
1178
|
const declaration = {
|
|
718
1179
|
name,
|
|
1180
|
+
type,
|
|
719
1181
|
/**
|
|
720
1182
|
* Only a plain object literal can be an `interface`. A permissive response is
|
|
721
1183
|
* `{ ...pinned } & Record<string, unknown>`, and `export interface X { ... } & ...` does not parse -
|
|
@@ -738,6 +1200,7 @@ export class TypeRegistry {
|
|
|
738
1200
|
*/
|
|
739
1201
|
isVocabulary: type.kind === "Enum",
|
|
740
1202
|
source,
|
|
1203
|
+
at,
|
|
741
1204
|
};
|
|
742
1205
|
this.#declarations.set(key, declaration);
|
|
743
1206
|
/**
|
|
@@ -759,14 +1222,27 @@ export class TypeRegistry {
|
|
|
759
1222
|
this.#order.push(declaration);
|
|
760
1223
|
}
|
|
761
1224
|
else if (sameName.source !== declaration.source) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1225
|
+
// Refused as the schema registry refuses it - one declaration, of nothing.
|
|
1226
|
+
reportCollision(this.#program, declaration.name, sameName, declaration);
|
|
1227
|
+
const refused = { ...sameName, source: "never", isObject: false };
|
|
1228
|
+
this.#order[this.#order.indexOf(sameName)] = refused;
|
|
1229
|
+
for (const [other, value] of this.#declarations) {
|
|
1230
|
+
if (value === sameName || value === declaration)
|
|
1231
|
+
this.#declarations.set(other, refused);
|
|
1232
|
+
}
|
|
767
1233
|
}
|
|
768
1234
|
return declaration.name;
|
|
769
1235
|
}
|
|
1236
|
+
/** Declare a component the document publishes, as {@link SchemaRegistry.declare} does. */
|
|
1237
|
+
declare(type) {
|
|
1238
|
+
this.#depth += 1;
|
|
1239
|
+
try {
|
|
1240
|
+
this.expressionFor(type);
|
|
1241
|
+
}
|
|
1242
|
+
finally {
|
|
1243
|
+
this.#depth -= 1;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
770
1246
|
/**
|
|
771
1247
|
* The Zod-free type for a single property - used for path and query parameters, which are
|
|
772
1248
|
* `ModelProperty`s belonging to no model. Without routing them through the registry a named model
|
|
@@ -774,17 +1250,102 @@ export class TypeRegistry {
|
|
|
774
1250
|
*/
|
|
775
1251
|
/** `wireName` names an HTTP parameter as the wire does - see {@link propertyToTs}. */
|
|
776
1252
|
expressionForProperty(property, wireName) {
|
|
777
|
-
return withTsRefResolver(
|
|
1253
|
+
return withParameterPosition(true, () => withTsRefResolver(
|
|
778
1254
|
// Through expressionFor even when undeclared, so the cycle guard above is reachable.
|
|
779
|
-
(candidate) => this.expressionFor(candidate), () =>
|
|
1255
|
+
(candidate) => this.expressionFor(candidate), () =>
|
|
1256
|
+
// A refusal points at the property, as the schema walk's does.
|
|
1257
|
+
withDeclarationSite(property, () => withHeldDeclarations(this.#held, () => propertyToTs(this.#program, property, wireName)))));
|
|
780
1258
|
}
|
|
781
|
-
#inline(type) {
|
|
782
|
-
|
|
1259
|
+
#inline(type, at, declared) {
|
|
1260
|
+
// Refused as the schema registry refuses it, which reports it.
|
|
1261
|
+
const ignore = metadataAnnotationsIgnored();
|
|
1262
|
+
if (isSplitByVisibility(this.#program, type, at, ignore, declared))
|
|
1263
|
+
return "never";
|
|
1264
|
+
this.#depth += 1;
|
|
1265
|
+
try {
|
|
1266
|
+
// A declaration is a component every use shares, a parameter's included.
|
|
1267
|
+
return declared === undefined
|
|
1268
|
+
? this.#body(type, at)
|
|
1269
|
+
: withParameterPosition(false, () => this.#body(type, at));
|
|
1270
|
+
}
|
|
1271
|
+
finally {
|
|
1272
|
+
this.#depth -= 1;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/** A model's body renders its own properties, outside a part's binary union - see `withMultipartBinary`. */
|
|
1276
|
+
#body(type, at) {
|
|
1277
|
+
return withMultipartBinary(multipartBinaryContext() && type.kind !== "Model", () => this.#bodyAt(type, at));
|
|
1278
|
+
}
|
|
1279
|
+
#bodyAt(type, at) {
|
|
1280
|
+
return withVisibility(at, () => withTsRefResolver(
|
|
783
1281
|
// Through expressionFor even when undeclared, so the cycle guard above is reachable.
|
|
784
|
-
(candidate) => this.expressionFor(candidate), () => typeToTsBody(this.#program, type));
|
|
1282
|
+
(candidate) => this.expressionFor(candidate), () => withTsSyntheticDeclarations((name, build, site) => this.#declareSynthetic(name, build, { kind: "envelope", site }), () => withHeldDeclarations(this.#held, () => typeToTsBody(this.#program, type)))));
|
|
1283
|
+
}
|
|
1284
|
+
/** A multipart body's type, under the component's name - see {@link SchemaRegistry.declareNamed}. */
|
|
1285
|
+
declareNamed(name, build, site) {
|
|
1286
|
+
return this.#declareSynthetic(name, build, { kind: "multipart", site });
|
|
1287
|
+
}
|
|
1288
|
+
/** A type for a shape the spec never wrote, as {@link SchemaRegistry.declareNamed} declares one. */
|
|
1289
|
+
#declareSynthetic(published, build, synthetic) {
|
|
1290
|
+
// Under the rule every declaration's name is decided by - see `safeDeclarationName`.
|
|
1291
|
+
const name = safeDeclarationName(published);
|
|
1292
|
+
const key = `synthetic\u0000${name}`;
|
|
1293
|
+
const existing = this.#declarations.get(key);
|
|
1294
|
+
if (existing !== undefined)
|
|
1295
|
+
return existing.name;
|
|
1296
|
+
if (this.#inProgress.has(key))
|
|
1297
|
+
return name;
|
|
1298
|
+
this.#inProgress.set(key, name);
|
|
1299
|
+
const source = build();
|
|
1300
|
+
this.#inProgress.delete(key);
|
|
1301
|
+
const declaration = {
|
|
1302
|
+
name,
|
|
1303
|
+
isObject: isSingleObjectLiteral(source),
|
|
1304
|
+
isVocabulary: false,
|
|
1305
|
+
source,
|
|
1306
|
+
synthetic,
|
|
1307
|
+
};
|
|
1308
|
+
// One name, one declaration, as the schema registry keeps it - see its `#declareSynthetic`.
|
|
1309
|
+
const twin = this.#order.find((candidate) => candidate.name === name);
|
|
1310
|
+
if (twin !== undefined && twin.source === source) {
|
|
1311
|
+
this.#declarations.set(key, twin);
|
|
1312
|
+
return name;
|
|
1313
|
+
}
|
|
1314
|
+
if (twin !== undefined) {
|
|
1315
|
+
reportCollision(this.#program, name, twin, declaration);
|
|
1316
|
+
const refused = { ...twin, source: "never", isObject: false };
|
|
1317
|
+
this.#order[this.#order.indexOf(twin)] = refused;
|
|
1318
|
+
for (const [other, value] of this.#declarations) {
|
|
1319
|
+
if (value === twin)
|
|
1320
|
+
this.#declarations.set(other, refused);
|
|
1321
|
+
}
|
|
1322
|
+
this.#declarations.set(key, refused);
|
|
1323
|
+
return name;
|
|
1324
|
+
}
|
|
1325
|
+
this.#declarations.set(key, declaration);
|
|
1326
|
+
this.#order.push(declaration);
|
|
1327
|
+
return name;
|
|
785
1328
|
}
|
|
786
1329
|
/** Every declaration, dependencies first. */
|
|
787
1330
|
declarations() {
|
|
788
1331
|
return this.#order;
|
|
789
1332
|
}
|
|
1333
|
+
/**
|
|
1334
|
+
* **A contract type whose validator validates nothing is `never` too**, whatever refused it: two
|
|
1335
|
+
* shapes under one name, a union this emitter refuses, a model openapi3 publishes split. The wire
|
|
1336
|
+
* assertion pairs the two by name, and a contract type stating a shape beside `z.never()` failed
|
|
1337
|
+
* it - `interface HBase { name: string }` beside a refused `hBaseSchema`.
|
|
1338
|
+
*/
|
|
1339
|
+
refuse(names) {
|
|
1340
|
+
for (const [index, declaration] of this.#order.entries()) {
|
|
1341
|
+
if (!names.has(declaration.name) || declaration.source === "never")
|
|
1342
|
+
continue;
|
|
1343
|
+
const refused = { ...declaration, source: "never", isObject: false };
|
|
1344
|
+
this.#order[index] = refused;
|
|
1345
|
+
for (const [key, value] of this.#declarations) {
|
|
1346
|
+
if (value === declaration)
|
|
1347
|
+
this.#declarations.set(key, refused);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
790
1351
|
}
|