typespec-http-zod 0.30.0 → 0.32.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 +22 -0
- package/dist/src/api.d.ts +31 -0
- package/dist/src/api.js +1475 -362
- package/dist/src/constraints.d.ts +31 -2
- package/dist/src/constraints.js +242 -46
- package/dist/src/lib.d.ts +163 -13
- package/dist/src/lib.js +207 -18
- package/dist/src/openapi-versions.d.ts +20 -0
- package/dist/src/openapi-versions.js +31 -0
- package/dist/src/published.d.ts +139 -0
- package/dist/src/published.js +798 -0
- package/dist/src/registry.d.ts +79 -45
- package/dist/src/registry.js +788 -200
- 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 +764 -39
- package/dist/src/zod.js +4313 -541
- 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, requestReadingAt, requestReadsApart, rootVisibilityOf, UNREPRESENTABLE, typeToZodBody, visibilityName, withDeclarationSite, withDeferredAnnotator, withMetadataAnnotationsIgnored, withMultipartBinary, withRefResolver, withRequestReadingAs, 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
|
-
*
|
|
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.
|
|
81
22
|
*
|
|
82
|
-
*
|
|
83
|
-
* `any`, `never` and `public`
|
|
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.
|
|
84
29
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 39 are refused and 39 are listed. `yield` had been recorded here as measured-legal and is not,
|
|
94
|
-
* which is why the arm that grades this set now takes its words from somewhere else entirely.
|
|
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,83 @@ 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
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* `isTransformedBy` is its own predicate, `metadataInfo.isTransformed`.
|
|
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.
|
|
244
405
|
*
|
|
245
|
-
* **
|
|
246
|
-
*
|
|
247
|
-
*
|
|
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);
|
|
266
431
|
}
|
|
267
|
-
function
|
|
432
|
+
export function nameAt(bare, at, type, reading) {
|
|
268
433
|
/**
|
|
269
|
-
* **
|
|
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.
|
|
434
|
+
* **A 3.0 request's reading of a shared schema is named for the request**, `WidgetCreate` beside
|
|
435
|
+
* the `Widget` a response reads: the document publishes one component and OAS 3.0 reads it two
|
|
436
|
+
* ways - see `withRequestReading` - so the module declares both, the request's under the name
|
|
437
|
+
* openapi3 gives a view of the type at that visibility.
|
|
276
438
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
439
|
+
if (reading !== undefined) {
|
|
440
|
+
return safeDeclarationName(`${bare}${getVisibilitySuffix(reading, Visibility.Read)}`);
|
|
441
|
+
}
|
|
442
|
+
// Suffixed only where openapi3 suffixes it: used at more than one visibility - see `isSuffixedByUsage`.
|
|
443
|
+
const suffixed = at !== Visibility.Read && isSuffixedByUsage(type);
|
|
444
|
+
return safeDeclarationName(`${bare}${suffixed ? getVisibilitySuffix(at, Visibility.Read) : ""}`);
|
|
279
445
|
}
|
|
280
|
-
/**
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
446
|
+
/**
|
|
447
|
+
* **The 3.0 request a shared schema is read apart for here**, or `undefined`: only a type rendered at
|
|
448
|
+
* `Read` is shared with a response, and only one reaching a required read-only property reads apart -
|
|
449
|
+
* see `requestReadsApart`. A view openapi3 publishes for a request's own visibility drops read-only
|
|
450
|
+
* properties already, and the shared types it holds are read apart as they are reached.
|
|
451
|
+
*/
|
|
452
|
+
function readingOf(program, type, at) {
|
|
453
|
+
const reading = requestReadingAt();
|
|
454
|
+
return reading !== undefined && at === Visibility.Read && requestReadsApart(program, type, at)
|
|
455
|
+
? reading
|
|
456
|
+
: undefined;
|
|
457
|
+
}
|
|
458
|
+
function keyFor(type, visibility, reading) {
|
|
459
|
+
/**
|
|
460
|
+
* **Keyed on the named model, never a copy of it**: `@typespec/http` synthesises a fresh anonymous
|
|
461
|
+
* model per position, and two operations taking one body each hand over a copy. Both resolve to
|
|
462
|
+
* the model itself, their effective payload type - see `payloadTypeOf` - so the document's one
|
|
463
|
+
* `BodyModel` is one declaration, where two copies of it once emitted `bodyModelSchema` twice.
|
|
464
|
+
*/
|
|
465
|
+
// Inside an explicit `@body` a type can render differently - see `withMetadataAnnotationsIgnored`.
|
|
466
|
+
const body = metadataAnnotationsIgnored() ? "\u0000body" : "";
|
|
467
|
+
// And inside a multipart part's union holding raw `bytes` - see `withMultipartBinary`.
|
|
468
|
+
const binary = multipartBinaryContext() ? "\u0000multipart" : "";
|
|
469
|
+
// And a 3.0 request's reading of a shared schema - see `readingOf`.
|
|
470
|
+
const request = reading === undefined ? "" : `\u0000request${String(reading)}`;
|
|
471
|
+
return `${String(visibility)}\u0000${String(type.name ?? "")}\u0000${identityOf(type)}${body}${binary}${request}`;
|
|
285
472
|
}
|
|
286
473
|
/** A stable per-type identity, since two models can share a name across namespaces. */
|
|
287
474
|
const identities = new WeakMap();
|
|
@@ -302,6 +489,12 @@ export class SchemaRegistry {
|
|
|
302
489
|
#inProgress = new Map();
|
|
303
490
|
/** Keys on a cycle - every one needs a structural type rather than a `z.infer` alias. */
|
|
304
491
|
#cyclic = new Set();
|
|
492
|
+
/** Identifiers refused as two shapes under one name, which no dispatch may hold. */
|
|
493
|
+
#refused = new Set();
|
|
494
|
+
/** Declarations refused as `split-visibility`, which a `@oneOf` holding them holds to nothing. */
|
|
495
|
+
#withheld = new Set();
|
|
496
|
+
/** Every type name a refused identifier stood for: `Foo` and `foo` are both `fooSchema`. */
|
|
497
|
+
#refusedTypeNames = new Set();
|
|
305
498
|
/**
|
|
306
499
|
* Inlined types currently being walked, by identity, against the number of declarations that were
|
|
307
500
|
* in flight when each was entered.
|
|
@@ -315,6 +508,8 @@ export class SchemaRegistry {
|
|
|
315
508
|
* entered since, the cycle has a name to close on and the walk may continue.
|
|
316
509
|
*/
|
|
317
510
|
#inlineInProgress = new Map();
|
|
511
|
+
/** How many bodies are being rendered - none, and the type asked for is an operation's own. */
|
|
512
|
+
#depth = 0;
|
|
318
513
|
constructor(program) {
|
|
319
514
|
this.#program = program;
|
|
320
515
|
}
|
|
@@ -323,8 +518,20 @@ export class SchemaRegistry {
|
|
|
323
518
|
* inline source. Declaring is depth-first so a model's dependencies are emitted above it, which is
|
|
324
519
|
* what lets the generated file be plain `const` declarations with no forward references.
|
|
325
520
|
*/
|
|
326
|
-
expressionFor(
|
|
521
|
+
expressionFor(given, payload = false) {
|
|
522
|
+
const type = payloadTypeOf(this.#program, given, payload, this.#depth);
|
|
523
|
+
// Published as a reference to another document, with no schema to validate.
|
|
524
|
+
const external = externalReferenceOf(type);
|
|
525
|
+
if (external !== undefined)
|
|
526
|
+
return refuseExternalReference(this.#program, type, external);
|
|
327
527
|
const bare = declaredNameOf(this.#program, type);
|
|
528
|
+
const requested = currentVisibility();
|
|
529
|
+
/**
|
|
530
|
+
* Asked of the model a declaration stands for, as openapi3 asks it of the effective payload type:
|
|
531
|
+
* a spread body's copy holds only what the request sends, so it is reshaped by nothing, while the
|
|
532
|
+
* model it copies - its `@path` property payload at `Read` - is.
|
|
533
|
+
*/
|
|
534
|
+
const at = visibilityAt(this.#program, type, requested, this.#depth);
|
|
328
535
|
if (bare === undefined) {
|
|
329
536
|
/**
|
|
330
537
|
* **A back edge to an INLINED type is a cycle with no name to close it**, and following it
|
|
@@ -342,7 +549,7 @@ export class SchemaRegistry {
|
|
|
342
549
|
}
|
|
343
550
|
this.#inlineInProgress.set(type, this.#inProgress.size);
|
|
344
551
|
try {
|
|
345
|
-
return this.#inline(type);
|
|
552
|
+
return this.#inline(type, at, undefined);
|
|
346
553
|
}
|
|
347
554
|
finally {
|
|
348
555
|
this.#inlineInProgress.delete(type);
|
|
@@ -357,8 +564,8 @@ export class SchemaRegistry {
|
|
|
357
564
|
* request and a response. Collapsing onto `Read` first is what makes the key and the name agree,
|
|
358
565
|
* and it is the same collapse openapi3 performs before naming a component.
|
|
359
566
|
*/
|
|
360
|
-
const
|
|
361
|
-
const key = keyFor(type, at);
|
|
567
|
+
const reading = readingOf(this.#program, type, at);
|
|
568
|
+
const key = keyFor(type, at, reading);
|
|
362
569
|
const existing = this.#declarations.get(key);
|
|
363
570
|
if (existing !== undefined)
|
|
364
571
|
return existing.identifier;
|
|
@@ -383,34 +590,21 @@ export class SchemaRegistry {
|
|
|
383
590
|
this.#cyclic.add(inFlight);
|
|
384
591
|
return pending;
|
|
385
592
|
}
|
|
386
|
-
const name = nameAt(bare, at);
|
|
593
|
+
const name = nameAt(bare, at, type, reading);
|
|
387
594
|
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)));
|
|
595
|
+
const { source, deferred, consulted } = this.#render(type, at, name, key, identifier);
|
|
408
596
|
const declaration = {
|
|
409
597
|
identifier,
|
|
410
598
|
typeName: name,
|
|
411
599
|
source,
|
|
412
|
-
|
|
413
|
-
|
|
600
|
+
type,
|
|
601
|
+
at,
|
|
602
|
+
key,
|
|
603
|
+
ignore: metadataAnnotationsIgnored(),
|
|
604
|
+
...(reading === undefined ? {} : { reading }),
|
|
605
|
+
...(this.#cyclic.has(key) ? { structural: this.#structuralBodyOf(type, at) } : {}),
|
|
606
|
+
...(deferred ? { annotated: true } : {}),
|
|
607
|
+
...(consulted.size === 0 ? {} : { consulted }),
|
|
414
608
|
};
|
|
415
609
|
/**
|
|
416
610
|
* **Two different types can resolve to one identifier, and the module then does not parse.**
|
|
@@ -429,19 +623,191 @@ export class SchemaRegistry {
|
|
|
429
623
|
* `duplicate-id`; neither resolves it for you, because only the author knows which name to
|
|
430
624
|
* change.
|
|
431
625
|
*/
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
626
|
+
const twin = this.#order.find((candidate) => candidate.identifier === identifier);
|
|
627
|
+
/**
|
|
628
|
+
* **One declaration reached in two contexts that render it alike is one declaration.** A type
|
|
629
|
+
* under an explicit `@body` is keyed apart from the same type elsewhere, since the body can
|
|
630
|
+
* publish a `@header` as data; where nothing in it does, the two are the same text and the
|
|
631
|
+
* document's one component.
|
|
632
|
+
*/
|
|
633
|
+
if (twin !== undefined && twin.source === declaration.source) {
|
|
634
|
+
this.#declarations.set(key, twin);
|
|
635
|
+
return twin.identifier;
|
|
636
|
+
}
|
|
637
|
+
if (twin !== undefined) {
|
|
638
|
+
reportCollision(this.#program, name, twin, declaration);
|
|
639
|
+
/**
|
|
640
|
+
* **One name, one declaration, and it validates nothing.** Two differing shapes under one
|
|
641
|
+
* identifier was a module declaring it twice, which does not parse. The document holds one
|
|
642
|
+
* of them under the name and openapi3 refuses its own document for it, so neither is kept.
|
|
643
|
+
*/
|
|
644
|
+
const refused = {
|
|
645
|
+
identifier,
|
|
646
|
+
typeName: name,
|
|
647
|
+
source: UNREPRESENTABLE,
|
|
648
|
+
...(twin.type === undefined ? {} : { type: twin.type }),
|
|
649
|
+
...(twin.at === undefined ? {} : { at: twin.at }),
|
|
650
|
+
};
|
|
651
|
+
this.#order[this.#order.indexOf(twin)] = refused;
|
|
652
|
+
for (const [other, value] of this.#declarations) {
|
|
653
|
+
if (value === twin)
|
|
654
|
+
this.#declarations.set(other, refused);
|
|
655
|
+
}
|
|
656
|
+
this.#declarations.set(key, refused);
|
|
657
|
+
this.#refused.add(identifier);
|
|
658
|
+
this.#refusedTypeNames.add(twin.typeName).add(name);
|
|
659
|
+
if (twin.type !== undefined)
|
|
660
|
+
this.#refuseEnvelopesOf(twin.type);
|
|
661
|
+
this.#renderDispatchesHolding(identifier);
|
|
662
|
+
return identifier;
|
|
439
663
|
}
|
|
440
664
|
this.#declarations.set(key, declaration);
|
|
441
665
|
this.#order.push(declaration);
|
|
442
666
|
this.#declareRelatives(type);
|
|
443
667
|
return declaration.identifier;
|
|
444
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* A declaration's body, rendered with its key in flight so a reference back to it is a back edge.
|
|
671
|
+
*
|
|
672
|
+
* A back edge that survived the whole body reached this declaration by a path with no object
|
|
673
|
+
* property on it - a named union variant, or a dictionary value - so nothing could make it lazy
|
|
674
|
+
* and the emitted `const` would read itself while initialising. `z.lazy()` defers the whole body
|
|
675
|
+
* instead, which is the only place left to put the deferral once no property can hold it.
|
|
676
|
+
*/
|
|
677
|
+
#render(type, at, name, key, identifier) {
|
|
678
|
+
this.#inProgress.set(key, identifier);
|
|
679
|
+
const consulted = new Set();
|
|
680
|
+
this.#consulted.push(consulted);
|
|
681
|
+
const rendered = (() => {
|
|
682
|
+
try {
|
|
683
|
+
return captureBackEdges(() => this.#inline(type, at, name));
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
this.#consulted.pop();
|
|
687
|
+
}
|
|
688
|
+
})();
|
|
689
|
+
this.#inProgress.delete(key);
|
|
690
|
+
if (rendered.value === UNREPRESENTABLE &&
|
|
691
|
+
isSplitByVisibility(this.#program, type, at, metadataAnnotationsIgnored(), name)) {
|
|
692
|
+
this.#withheld.add(identifier);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* **The description goes on the declaration's own body, so the exported const IS the
|
|
696
|
+
* described schema.** Every reference then names that one instance, which is what keeps
|
|
697
|
+
* identity, `z.compile`'s clone (it inherits the description through its parent link) and
|
|
698
|
+
* Zod's `$ref` extraction all intact. Described at a reference instead, each use would be a
|
|
699
|
+
* fresh clone. A visibility projection carries the doc of the model it projects, as openapi3
|
|
700
|
+
* publishes one component for both.
|
|
701
|
+
*/
|
|
702
|
+
const body = rendered.deferred ? `z.lazy(() => ${rendered.value})` : rendered.value;
|
|
703
|
+
const source = described(body, getDoc(this.#program, type));
|
|
704
|
+
return { source, deferred: rendered.deferred, consulted };
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* **Every dispatch rendered before a subtype it holds was refused, rendered again without it.** A
|
|
708
|
+
* refusal is found where the second shape under one name is declared, and a base's dispatch can
|
|
709
|
+
* already hold the first: `z.discriminatedUnion` given the `z.never()` the refusal leaves throws
|
|
710
|
+
* on every parse, and the module does not typecheck. Each is rendered in its own context, its key
|
|
711
|
+
* in flight, so a reference back to it is still a back edge.
|
|
712
|
+
*/
|
|
713
|
+
/**
|
|
714
|
+
* **A union refused takes its envelopes with it.** openapi3 publishes each envelope wherever it
|
|
715
|
+
* publishes the union, under one name: where the union is two schemas under its one name, each
|
|
716
|
+
* envelope is too, and the document keeps the one written last - `DU7Y` with its `kind` and
|
|
717
|
+
* `value` optional, written from a PATCH - while its validator held the view it was first declared
|
|
718
|
+
* at, requiring both. Refused with the union, whose diagnostic says why; the contract types follow.
|
|
719
|
+
*/
|
|
720
|
+
#refuseEnvelopesOf(union) {
|
|
721
|
+
for (const [index, declaration] of this.#order.entries()) {
|
|
722
|
+
if (declaration.synthetic?.kind !== "envelope" || declaration.synthetic.site !== union)
|
|
723
|
+
continue;
|
|
724
|
+
if (declaration.source === UNREPRESENTABLE)
|
|
725
|
+
continue;
|
|
726
|
+
const refused = { ...declaration, source: UNREPRESENTABLE };
|
|
727
|
+
this.#order[index] = refused;
|
|
728
|
+
for (const [other, value] of this.#declarations) {
|
|
729
|
+
if (value === declaration)
|
|
730
|
+
this.#declarations.set(other, refused);
|
|
731
|
+
}
|
|
732
|
+
this.#refused.add(declaration.identifier);
|
|
733
|
+
this.#refusedTypeNames.add(declaration.typeName);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/** What a value rendered here may ask of the declarations it refers to - see `HeldDeclarations`. */
|
|
737
|
+
#held = {
|
|
738
|
+
refused: (expression) => this.#refused.has(expression),
|
|
739
|
+
annotated: (expression) => this.#order.some((declaration) => declaration.identifier === expression && declaration.annotated === true),
|
|
740
|
+
refusedAt: (type, at) => {
|
|
741
|
+
const key = keyFor(type, at, readingOf(this.#program, type, at));
|
|
742
|
+
this.#consulted.at(-1)?.add(key);
|
|
743
|
+
const declared = this.#declarations.get(key);
|
|
744
|
+
return (declared !== undefined &&
|
|
745
|
+
(this.#refused.has(declared.identifier) || this.#withheld.has(declared.identifier)));
|
|
746
|
+
},
|
|
747
|
+
holdsRefusalAt: (type, at) => {
|
|
748
|
+
const key = keyFor(type, at, readingOf(this.#program, type, at));
|
|
749
|
+
this.#consulted.at(-1)?.add(key);
|
|
750
|
+
const declared = this.#declarations.get(key);
|
|
751
|
+
return declared !== undefined && this.#reachesRefusal(declared.identifier, new Set());
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
/** Whether a declaration is refused, or its source refers to one that reaches a refusal. */
|
|
755
|
+
#reachesRefusal(identifier, seen) {
|
|
756
|
+
if (seen.has(identifier))
|
|
757
|
+
return false;
|
|
758
|
+
seen.add(identifier);
|
|
759
|
+
if (this.#refused.has(identifier) || this.#withheld.has(identifier))
|
|
760
|
+
return true;
|
|
761
|
+
const declaration = this.#order.find((each) => each.identifier === identifier);
|
|
762
|
+
if (declaration === undefined)
|
|
763
|
+
return false;
|
|
764
|
+
return [...declaration.source.matchAll(/[A-Za-z_$][\w$]*/g)].some(([name]) => name !== identifier && this.#declares(name) && this.#reachesRefusal(name, seen));
|
|
765
|
+
}
|
|
766
|
+
#declares(identifier) {
|
|
767
|
+
return this.#order.some((each) => each.identifier === identifier);
|
|
768
|
+
}
|
|
769
|
+
/** The keys each declaration in flight asked `refusedAt` of, innermost last. */
|
|
770
|
+
#consulted = [];
|
|
771
|
+
/** What the contract types' walk asks of these declarations, so a merge is `never` on both sides. */
|
|
772
|
+
get held() {
|
|
773
|
+
return this.#held;
|
|
774
|
+
}
|
|
775
|
+
#renderDispatchesHolding(identifier) {
|
|
776
|
+
const holds = new RegExp(`\\b${identifier}\\b`);
|
|
777
|
+
const refusedKeys = new Set([...this.#declarations]
|
|
778
|
+
.filter(([, value]) => value.identifier === identifier)
|
|
779
|
+
.map(([each]) => each));
|
|
780
|
+
// By index: rendering one again can declare more, which hold no refused subtype.
|
|
781
|
+
for (let index = 0; index < this.#order.length; index += 1) {
|
|
782
|
+
const held = this.#order[index];
|
|
783
|
+
const { type, at, key } = held;
|
|
784
|
+
if (type === undefined || at === undefined || key === undefined)
|
|
785
|
+
continue;
|
|
786
|
+
// A dispatch, or a `@oneOf` that now holds a refused member - see `holdsRefusedMember` -
|
|
787
|
+
// directly or through the declarations it refers to.
|
|
788
|
+
const dispatches = (held.source.includes("z.discriminatedUnion(") || held.source.includes("z.xor(")) &&
|
|
789
|
+
(holds.test(held.source) || this.#reachesRefusal(held.identifier, new Set()));
|
|
790
|
+
// A merge that asked before the refusal holds the shape it now refuses - see `refusedAt`.
|
|
791
|
+
const merges = [...(held.consulted ?? [])].some((each) => refusedKeys.has(each));
|
|
792
|
+
if (!dispatches && !merges)
|
|
793
|
+
continue;
|
|
794
|
+
const keys = [...this.#declarations]
|
|
795
|
+
.filter(([, value]) => value === held)
|
|
796
|
+
.map(([each]) => each);
|
|
797
|
+
for (const each of keys)
|
|
798
|
+
this.#declarations.delete(each);
|
|
799
|
+
const { source, deferred, consulted } = withRequestReadingAs(held.reading, () => withMetadataAnnotationsIgnored(held.ignore ?? false, () => this.#render(type, at, held.typeName, key, held.identifier)));
|
|
800
|
+
const fresh = {
|
|
801
|
+
...held,
|
|
802
|
+
source,
|
|
803
|
+
...(deferred ? { annotated: true } : {}),
|
|
804
|
+
consulted,
|
|
805
|
+
};
|
|
806
|
+
this.#order[index] = fresh;
|
|
807
|
+
for (const each of keys)
|
|
808
|
+
this.#declarations.set(each, fresh);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
445
811
|
/**
|
|
446
812
|
* Declare the BASE CHAIN a component's own declaration implies.
|
|
447
813
|
*
|
|
@@ -451,8 +817,8 @@ export class SchemaRegistry {
|
|
|
451
817
|
* them.
|
|
452
818
|
*
|
|
453
819
|
* **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
|
-
*
|
|
820
|
+
* discriminated base renders as a dispatch over its subtypes - `z.discriminatedUnion`, beside the base
|
|
821
|
+
* itself - and rendering that already asks the registry for each one. A loop here looked load-bearing and was measured not to
|
|
456
822
|
* be: removing it left all 23 polymorphism and differential arms green.
|
|
457
823
|
*
|
|
458
824
|
* **Wrapped in {@link captureBackEdges} and the result discarded.** This call can land on a
|
|
@@ -466,14 +832,35 @@ export class SchemaRegistry {
|
|
|
466
832
|
const base = type.baseModel;
|
|
467
833
|
captureBackEdges(() => this.expressionFor(base));
|
|
468
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Declare a component the document publishes, at the visibility the walk is at, as a type
|
|
837
|
+
* another refers to - see `publishedComponents`. Nothing refers to it here, so a declaration
|
|
838
|
+
* still in flight is no back edge.
|
|
839
|
+
*/
|
|
840
|
+
declare(type) {
|
|
841
|
+
this.#depth += 1;
|
|
842
|
+
try {
|
|
843
|
+
captureBackEdges(() => this.expressionFor(type));
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
this.#depth -= 1;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
469
849
|
/**
|
|
470
850
|
* The Zod for a single property, constraints and optionality included.
|
|
471
851
|
*
|
|
472
852
|
* Used for path/query parameters, which are `ModelProperty`s that belong to no model - so they
|
|
473
853
|
* need the property-level walk rather than the type-level one, or `@minLength` on a path param
|
|
474
854
|
* would be documented and never enforced.
|
|
855
|
+
*
|
|
856
|
+
* **The property is where a refusal inside it points**, as a model's member is - see
|
|
857
|
+
* `withDeclarationSite`. A parameter is walked outside any model, so `@query a?: never[]` was
|
|
858
|
+
* reported at `<unknown location>:1:1`, the intrinsic's own place, which names nothing to edit.
|
|
475
859
|
*/
|
|
476
|
-
expressionForProperty(property) {
|
|
860
|
+
expressionForProperty(property, position = OWN_POSITION) {
|
|
861
|
+
return withParameterPosition(true, () => withDeclarationSite(property, () => this.#parameter(property, position)));
|
|
862
|
+
}
|
|
863
|
+
#parameter(property, position) {
|
|
477
864
|
return withRefResolver(
|
|
478
865
|
/**
|
|
479
866
|
* **Every candidate goes through {@link expressionFor}, including the ones with no name.**
|
|
@@ -485,7 +872,7 @@ export class SchemaRegistry {
|
|
|
485
872
|
* `expressionFor` renders an undeclared type through `#inline` exactly as before, and now
|
|
486
873
|
* also notices when it is already doing so.
|
|
487
874
|
*/
|
|
488
|
-
(candidate) => this.expressionFor(candidate), () => propertyToZod(this.#program, property));
|
|
875
|
+
(candidate) => this.expressionFor(candidate), () => withHeldDeclarations(this.#held, () => propertyToZod(this.#program, property, position)));
|
|
489
876
|
}
|
|
490
877
|
/**
|
|
491
878
|
* A declaration for a shape the spec never wrote, under a name the document publishes.
|
|
@@ -499,10 +886,12 @@ export class SchemaRegistry {
|
|
|
499
886
|
* body: the document publishes `MultiPartRequest` as a component, and there is no walkable type
|
|
500
887
|
* for it - the parts have to be assembled from `@typespec/http`'s own view of the payload.
|
|
501
888
|
*/
|
|
502
|
-
declareNamed(name, build) {
|
|
503
|
-
return this.#declareSynthetic(name, build);
|
|
889
|
+
declareNamed(name, build, site) {
|
|
890
|
+
return this.#declareSynthetic(name, build, { kind: "multipart", site });
|
|
504
891
|
}
|
|
505
|
-
#declareSynthetic(
|
|
892
|
+
#declareSynthetic(published, build, synthetic) {
|
|
893
|
+
// Under the rule every declaration's name is decided by - see `safeDeclarationName`.
|
|
894
|
+
const name = safeDeclarationName(published);
|
|
506
895
|
const key = `synthetic\u0000${name}`;
|
|
507
896
|
const existing = this.#declarations.get(key);
|
|
508
897
|
if (existing !== undefined)
|
|
@@ -513,7 +902,31 @@ export class SchemaRegistry {
|
|
|
513
902
|
this.#inProgress.set(key, identifier);
|
|
514
903
|
const source = build();
|
|
515
904
|
this.#inProgress.delete(key);
|
|
516
|
-
const declaration = { identifier, typeName: name, source };
|
|
905
|
+
const declaration = { identifier, typeName: name, source, synthetic };
|
|
906
|
+
/**
|
|
907
|
+
* **A name a type already declares is the same collision as any other**: a model sent as JSON
|
|
908
|
+
* and as a multipart form, its JSON component declared first, declared `formSchema` twice, and
|
|
909
|
+
* the module did not parse. One rendered alike is the one declaration.
|
|
910
|
+
*/
|
|
911
|
+
const twin = this.#order.find((candidate) => candidate.identifier === identifier);
|
|
912
|
+
if (twin !== undefined && twin.source === source) {
|
|
913
|
+
this.#declarations.set(key, twin);
|
|
914
|
+
return identifier;
|
|
915
|
+
}
|
|
916
|
+
if (twin !== undefined) {
|
|
917
|
+
reportCollision(this.#program, name, twin, declaration);
|
|
918
|
+
const refused = { ...twin, source: UNREPRESENTABLE };
|
|
919
|
+
this.#order[this.#order.indexOf(twin)] = refused;
|
|
920
|
+
for (const [other, value] of this.#declarations) {
|
|
921
|
+
if (value === twin)
|
|
922
|
+
this.#declarations.set(other, refused);
|
|
923
|
+
}
|
|
924
|
+
this.#declarations.set(key, refused);
|
|
925
|
+
this.#refused.add(identifier);
|
|
926
|
+
this.#refusedTypeNames.add(twin.typeName).add(name);
|
|
927
|
+
this.#renderDispatchesHolding(identifier);
|
|
928
|
+
return identifier;
|
|
929
|
+
}
|
|
517
930
|
this.#declarations.set(key, declaration);
|
|
518
931
|
this.#order.push(declaration);
|
|
519
932
|
return identifier;
|
|
@@ -532,8 +945,27 @@ export class SchemaRegistry {
|
|
|
532
945
|
* `wire-contract.gen.ts`, and a third spelling of "what this shape is" would be a third thing to
|
|
533
946
|
* drift.
|
|
534
947
|
*/
|
|
535
|
-
#structuralBodyOf(type) {
|
|
536
|
-
|
|
948
|
+
#structuralBodyOf(type, at) {
|
|
949
|
+
/**
|
|
950
|
+
* At the visibility the declaration is keyed on, as its body was rendered: read at the one it
|
|
951
|
+
* was requested at, a discriminated base reached from a create body annotated itself with
|
|
952
|
+
* `DogCreate`, a declaration nothing made.
|
|
953
|
+
*/
|
|
954
|
+
return withVisibility(at, () => withParameterPosition(false, () => withTsRefResolver((candidate) => this.#typeNameOf(candidate) ?? this.#reducedBodyOf(candidate), () => typeToTsBody(this.#program, type))));
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* **A type with no declaration of its own is written out where openapi3 reduces it to**, as
|
|
958
|
+
* {@link TypeRegistry.expressionFor} writes it: `q?: Record<M4>` under a PATCH is a `Record`
|
|
959
|
+
* openapi3 renders at `Read`, its value `M4`. Walked at the PATCH, the annotation named
|
|
960
|
+
* `M4Update`, which nothing declares (`TS2552`). `undefined` where the visibility stands, so the
|
|
961
|
+
* walk goes on as it was.
|
|
962
|
+
*/
|
|
963
|
+
#reducedBodyOf(candidate) {
|
|
964
|
+
const requested = currentVisibility();
|
|
965
|
+
const at = renderedVisibilityOf(this.#program, candidate, requested);
|
|
966
|
+
return at === requested
|
|
967
|
+
? undefined
|
|
968
|
+
: withVisibility(at, () => typeToTsBody(this.#program, candidate));
|
|
537
969
|
}
|
|
538
970
|
/**
|
|
539
971
|
* **The TypeScript name a reference resolves to is the one {@link expressionFor} declares it
|
|
@@ -546,7 +978,8 @@ export class SchemaRegistry {
|
|
|
546
978
|
const bare = declaredNameOf(this.#program, candidate);
|
|
547
979
|
if (bare === undefined)
|
|
548
980
|
return undefined;
|
|
549
|
-
|
|
981
|
+
const at = renderedVisibilityOf(this.#program, candidate, currentVisibility());
|
|
982
|
+
return nameAt(bare, at, candidate, readingOf(this.#program, candidate, at));
|
|
550
983
|
}
|
|
551
984
|
/**
|
|
552
985
|
* Render `type` without consulting its own declaration, but letting nested types use theirs.
|
|
@@ -558,8 +991,28 @@ export class SchemaRegistry {
|
|
|
558
991
|
* `RangeError: Maximum call stack size exceeded`. Skipping the resolver once, here, is what makes
|
|
559
992
|
* a nested occurrence resolve to the identifier while the body still renders as a body.
|
|
560
993
|
*/
|
|
561
|
-
#inline(type) {
|
|
562
|
-
|
|
994
|
+
#inline(type, at, declared) {
|
|
995
|
+
// What openapi3 publishes with its properties read at another visibility - see `published.ts`.
|
|
996
|
+
const ignore = metadataAnnotationsIgnored();
|
|
997
|
+
if (isSplitByVisibility(this.#program, type, at, ignore, declared))
|
|
998
|
+
return UNREPRESENTABLE;
|
|
999
|
+
this.#depth += 1;
|
|
1000
|
+
try {
|
|
1001
|
+
// A declaration is a component every use shares, a parameter's included.
|
|
1002
|
+
return declared === undefined
|
|
1003
|
+
? this.#body(type, at)
|
|
1004
|
+
: withParameterPosition(false, () => this.#body(type, at));
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
this.#depth -= 1;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/** A model's body renders its own properties, outside a part's binary union - see `withMultipartBinary`. */
|
|
1011
|
+
#body(type, at) {
|
|
1012
|
+
return withMultipartBinary(multipartBinaryContext() && type.kind !== "Model", () => this.#bodyAt(type, at));
|
|
1013
|
+
}
|
|
1014
|
+
#bodyAt(type, at) {
|
|
1015
|
+
return withVisibility(at, () => withRefResolver(
|
|
563
1016
|
/**
|
|
564
1017
|
* **Every candidate goes through {@link expressionFor}, including the ones with no name.**
|
|
565
1018
|
*
|
|
@@ -570,7 +1023,7 @@ export class SchemaRegistry {
|
|
|
570
1023
|
* `expressionFor` renders an undeclared type through `#inline` exactly as before, and now
|
|
571
1024
|
* also notices when it is already doing so.
|
|
572
1025
|
*/
|
|
573
|
-
(candidate) => this.expressionFor(candidate), () => withDeferredAnnotator((property) => this.#deferredAnnotationOf(property), () => withSyntheticDeclarations((name, build) => this.#declareSynthetic(name, build), () => typeToZodBody(this.#program, type))));
|
|
1026
|
+
(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
1027
|
}
|
|
575
1028
|
/**
|
|
576
1029
|
* The Zod type a property emitted as a getter is annotated with, or `undefined` for none.
|
|
@@ -587,18 +1040,32 @@ export class SchemaRegistry {
|
|
|
587
1040
|
* getter that its own value does not satisfy. A recursive property carrying a default is the one
|
|
588
1041
|
* shape this leaves as it was.
|
|
589
1042
|
*/
|
|
590
|
-
#deferredAnnotationOf(property) {
|
|
1043
|
+
#deferredAnnotationOf(property, position) {
|
|
591
1044
|
if (property.defaultValue !== undefined)
|
|
592
1045
|
return undefined;
|
|
593
|
-
const declared = withTsRefResolver((candidate) => this.#typeNameOf(candidate), () => propertyTypeToTs(this.#program, property));
|
|
1046
|
+
const declared = withTsRefResolver((candidate) => this.#typeNameOf(candidate), () => propertyTypeToTs(this.#program, property, position));
|
|
594
1047
|
const core = `z.ZodType<${declared}, ${declared}>`;
|
|
595
|
-
const optional = property.optional || isOptionalAt(this.#program, property);
|
|
1048
|
+
const optional = !position.required && (property.optional || isOptionalAt(this.#program, property));
|
|
596
1049
|
return `: ${optional ? `z.ZodExactOptional<${core}>` : core}`;
|
|
597
1050
|
}
|
|
1051
|
+
/** The annotation for a deferred value no property declares - an envelope's variant. */
|
|
1052
|
+
#deferredTypeAnnotationOf(type) {
|
|
1053
|
+
const declared = withTsRefResolver((candidate) => this.#typeNameOf(candidate), () => typeToTs(this.#program, type));
|
|
1054
|
+
return `: z.ZodType<${declared}, ${declared}>`;
|
|
1055
|
+
}
|
|
598
1056
|
/** Every declaration, dependencies first. */
|
|
599
1057
|
declarations() {
|
|
600
1058
|
return this.#order;
|
|
601
1059
|
}
|
|
1060
|
+
/** The names of the declarations that validate nothing, refused here or in the walk beneath. */
|
|
1061
|
+
refusedNames() {
|
|
1062
|
+
return new Set([
|
|
1063
|
+
...this.#refusedTypeNames,
|
|
1064
|
+
...this.#order
|
|
1065
|
+
.filter((declaration) => declaration.source === UNREPRESENTABLE)
|
|
1066
|
+
.map((declaration) => declaration.typeName),
|
|
1067
|
+
]);
|
|
1068
|
+
}
|
|
602
1069
|
}
|
|
603
1070
|
/**
|
|
604
1071
|
* The same walk as {@link SchemaRegistry}, emitting plain TypeScript instead of Zod.
|
|
@@ -622,7 +1089,7 @@ export class SchemaRegistry {
|
|
|
622
1089
|
* whole body is a single `{...}`, whatever it contains. A composition operator cannot be missed,
|
|
623
1090
|
* because anything following the matching close brace fails the test whatever it happens to be.
|
|
624
1091
|
*/
|
|
625
|
-
function isSingleObjectLiteral(source) {
|
|
1092
|
+
export function isSingleObjectLiteral(source) {
|
|
626
1093
|
if (!source.startsWith("{"))
|
|
627
1094
|
return false;
|
|
628
1095
|
let depth = 0;
|
|
@@ -661,7 +1128,7 @@ export class TypeRegistry {
|
|
|
661
1128
|
* projection. The two registries pair BY NAME in the emitted assertion, so they have to agree
|
|
662
1129
|
* about how many declarations there are before the pairing means anything.
|
|
663
1130
|
*
|
|
664
|
-
* Both now go through the shared
|
|
1131
|
+
* Both now go through the shared `renderedVisibilityOf` and {@link nameAt}: paraphrasing either
|
|
665
1132
|
* rule in one walk is how a validator ends up compared against a type describing something else.
|
|
666
1133
|
*/
|
|
667
1134
|
#declarations = new Map();
|
|
@@ -675,26 +1142,47 @@ export class TypeRegistry {
|
|
|
675
1142
|
* Reported only by the schema walk, so one spec does not produce the same complaint twice.
|
|
676
1143
|
*/
|
|
677
1144
|
#inlineInProgress = new Map();
|
|
678
|
-
|
|
1145
|
+
/** How many bodies are being rendered - see the schema registry's. */
|
|
1146
|
+
#depth = 0;
|
|
1147
|
+
/** The validators' declarations, whose refusals a merge here holds to as theirs do. */
|
|
1148
|
+
#held;
|
|
1149
|
+
constructor(program, held) {
|
|
679
1150
|
this.#program = program;
|
|
1151
|
+
this.#held = held ?? {
|
|
1152
|
+
refused: () => false,
|
|
1153
|
+
annotated: () => false,
|
|
1154
|
+
refusedAt: () => false,
|
|
1155
|
+
holdsRefusalAt: () => false,
|
|
1156
|
+
};
|
|
680
1157
|
}
|
|
681
|
-
expressionFor(
|
|
1158
|
+
expressionFor(given, payload = false) {
|
|
1159
|
+
const type = payloadTypeOf(this.#program, given, payload, this.#depth);
|
|
1160
|
+
// Refused by the schema walk, which reports it - see `externalReferenceOf`.
|
|
1161
|
+
if (externalReferenceOf(type) !== undefined)
|
|
1162
|
+
return "never";
|
|
682
1163
|
const bare = declaredNameOf(this.#program, type);
|
|
1164
|
+
const requested = currentVisibility();
|
|
1165
|
+
/**
|
|
1166
|
+
* Asked of the model a declaration stands for, as openapi3 asks it of the effective payload type:
|
|
1167
|
+
* a spread body's copy holds only what the request sends, so it is reshaped by nothing, while the
|
|
1168
|
+
* model it copies - its `@path` property payload at `Read` - is.
|
|
1169
|
+
*/
|
|
1170
|
+
const at = visibilityAt(this.#program, type, requested, this.#depth);
|
|
683
1171
|
if (bare === undefined) {
|
|
684
1172
|
const entered = this.#inlineInProgress.get(type);
|
|
685
1173
|
if (entered !== undefined && this.#inProgress.size <= entered)
|
|
686
1174
|
return "never";
|
|
687
1175
|
this.#inlineInProgress.set(type, this.#inProgress.size);
|
|
688
1176
|
try {
|
|
689
|
-
return this.#inline(type);
|
|
1177
|
+
return this.#inline(type, at, undefined);
|
|
690
1178
|
}
|
|
691
1179
|
finally {
|
|
692
1180
|
this.#inlineInProgress.delete(type);
|
|
693
1181
|
}
|
|
694
1182
|
}
|
|
695
|
-
const
|
|
696
|
-
const key = keyFor(type, at);
|
|
697
|
-
const name = nameAt(bare, at);
|
|
1183
|
+
const reading = readingOf(this.#program, type, at);
|
|
1184
|
+
const key = keyFor(type, at, reading);
|
|
1185
|
+
const name = nameAt(bare, at, type, reading);
|
|
698
1186
|
const existing = this.#declarations.get(key);
|
|
699
1187
|
if (existing !== undefined)
|
|
700
1188
|
return existing.name;
|
|
@@ -712,10 +1200,11 @@ export class TypeRegistry {
|
|
|
712
1200
|
if (pending !== undefined)
|
|
713
1201
|
return pending;
|
|
714
1202
|
this.#inProgress.set(key, name);
|
|
715
|
-
const source = this.#inline(type);
|
|
1203
|
+
const source = this.#inline(type, at, name);
|
|
716
1204
|
this.#inProgress.delete(key);
|
|
717
1205
|
const declaration = {
|
|
718
1206
|
name,
|
|
1207
|
+
type,
|
|
719
1208
|
/**
|
|
720
1209
|
* Only a plain object literal can be an `interface`. A permissive response is
|
|
721
1210
|
* `{ ...pinned } & Record<string, unknown>`, and `export interface X { ... } & ...` does not parse -
|
|
@@ -738,6 +1227,7 @@ export class TypeRegistry {
|
|
|
738
1227
|
*/
|
|
739
1228
|
isVocabulary: type.kind === "Enum",
|
|
740
1229
|
source,
|
|
1230
|
+
at,
|
|
741
1231
|
};
|
|
742
1232
|
this.#declarations.set(key, declaration);
|
|
743
1233
|
/**
|
|
@@ -759,14 +1249,27 @@ export class TypeRegistry {
|
|
|
759
1249
|
this.#order.push(declaration);
|
|
760
1250
|
}
|
|
761
1251
|
else if (sameName.source !== declaration.source) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1252
|
+
// Refused as the schema registry refuses it - one declaration, of nothing.
|
|
1253
|
+
reportCollision(this.#program, declaration.name, sameName, declaration);
|
|
1254
|
+
const refused = { ...sameName, source: "never", isObject: false };
|
|
1255
|
+
this.#order[this.#order.indexOf(sameName)] = refused;
|
|
1256
|
+
for (const [other, value] of this.#declarations) {
|
|
1257
|
+
if (value === sameName || value === declaration)
|
|
1258
|
+
this.#declarations.set(other, refused);
|
|
1259
|
+
}
|
|
767
1260
|
}
|
|
768
1261
|
return declaration.name;
|
|
769
1262
|
}
|
|
1263
|
+
/** Declare a component the document publishes, as {@link SchemaRegistry.declare} does. */
|
|
1264
|
+
declare(type) {
|
|
1265
|
+
this.#depth += 1;
|
|
1266
|
+
try {
|
|
1267
|
+
this.expressionFor(type);
|
|
1268
|
+
}
|
|
1269
|
+
finally {
|
|
1270
|
+
this.#depth -= 1;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
770
1273
|
/**
|
|
771
1274
|
* The Zod-free type for a single property - used for path and query parameters, which are
|
|
772
1275
|
* `ModelProperty`s belonging to no model. Without routing them through the registry a named model
|
|
@@ -774,17 +1277,102 @@ export class TypeRegistry {
|
|
|
774
1277
|
*/
|
|
775
1278
|
/** `wireName` names an HTTP parameter as the wire does - see {@link propertyToTs}. */
|
|
776
1279
|
expressionForProperty(property, wireName) {
|
|
777
|
-
return withTsRefResolver(
|
|
1280
|
+
return withParameterPosition(true, () => withTsRefResolver(
|
|
778
1281
|
// Through expressionFor even when undeclared, so the cycle guard above is reachable.
|
|
779
|
-
(candidate) => this.expressionFor(candidate), () =>
|
|
1282
|
+
(candidate) => this.expressionFor(candidate), () =>
|
|
1283
|
+
// A refusal points at the property, as the schema walk's does.
|
|
1284
|
+
withDeclarationSite(property, () => withHeldDeclarations(this.#held, () => propertyToTs(this.#program, property, wireName)))));
|
|
1285
|
+
}
|
|
1286
|
+
#inline(type, at, declared) {
|
|
1287
|
+
// Refused as the schema registry refuses it, which reports it.
|
|
1288
|
+
const ignore = metadataAnnotationsIgnored();
|
|
1289
|
+
if (isSplitByVisibility(this.#program, type, at, ignore, declared))
|
|
1290
|
+
return "never";
|
|
1291
|
+
this.#depth += 1;
|
|
1292
|
+
try {
|
|
1293
|
+
// A declaration is a component every use shares, a parameter's included.
|
|
1294
|
+
return declared === undefined
|
|
1295
|
+
? this.#body(type, at)
|
|
1296
|
+
: withParameterPosition(false, () => this.#body(type, at));
|
|
1297
|
+
}
|
|
1298
|
+
finally {
|
|
1299
|
+
this.#depth -= 1;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/** A model's body renders its own properties, outside a part's binary union - see `withMultipartBinary`. */
|
|
1303
|
+
#body(type, at) {
|
|
1304
|
+
return withMultipartBinary(multipartBinaryContext() && type.kind !== "Model", () => this.#bodyAt(type, at));
|
|
780
1305
|
}
|
|
781
|
-
#
|
|
782
|
-
return withTsRefResolver(
|
|
1306
|
+
#bodyAt(type, at) {
|
|
1307
|
+
return withVisibility(at, () => withTsRefResolver(
|
|
783
1308
|
// Through expressionFor even when undeclared, so the cycle guard above is reachable.
|
|
784
|
-
(candidate) => this.expressionFor(candidate), () => typeToTsBody(this.#program, type));
|
|
1309
|
+
(candidate) => this.expressionFor(candidate), () => withTsSyntheticDeclarations((name, build, site) => this.#declareSynthetic(name, build, { kind: "envelope", site }), () => withHeldDeclarations(this.#held, () => typeToTsBody(this.#program, type)))));
|
|
1310
|
+
}
|
|
1311
|
+
/** A multipart body's type, under the component's name - see {@link SchemaRegistry.declareNamed}. */
|
|
1312
|
+
declareNamed(name, build, site) {
|
|
1313
|
+
return this.#declareSynthetic(name, build, { kind: "multipart", site });
|
|
1314
|
+
}
|
|
1315
|
+
/** A type for a shape the spec never wrote, as {@link SchemaRegistry.declareNamed} declares one. */
|
|
1316
|
+
#declareSynthetic(published, build, synthetic) {
|
|
1317
|
+
// Under the rule every declaration's name is decided by - see `safeDeclarationName`.
|
|
1318
|
+
const name = safeDeclarationName(published);
|
|
1319
|
+
const key = `synthetic\u0000${name}`;
|
|
1320
|
+
const existing = this.#declarations.get(key);
|
|
1321
|
+
if (existing !== undefined)
|
|
1322
|
+
return existing.name;
|
|
1323
|
+
if (this.#inProgress.has(key))
|
|
1324
|
+
return name;
|
|
1325
|
+
this.#inProgress.set(key, name);
|
|
1326
|
+
const source = build();
|
|
1327
|
+
this.#inProgress.delete(key);
|
|
1328
|
+
const declaration = {
|
|
1329
|
+
name,
|
|
1330
|
+
isObject: isSingleObjectLiteral(source),
|
|
1331
|
+
isVocabulary: false,
|
|
1332
|
+
source,
|
|
1333
|
+
synthetic,
|
|
1334
|
+
};
|
|
1335
|
+
// One name, one declaration, as the schema registry keeps it - see its `#declareSynthetic`.
|
|
1336
|
+
const twin = this.#order.find((candidate) => candidate.name === name);
|
|
1337
|
+
if (twin !== undefined && twin.source === source) {
|
|
1338
|
+
this.#declarations.set(key, twin);
|
|
1339
|
+
return name;
|
|
1340
|
+
}
|
|
1341
|
+
if (twin !== undefined) {
|
|
1342
|
+
reportCollision(this.#program, name, twin, declaration);
|
|
1343
|
+
const refused = { ...twin, source: "never", isObject: false };
|
|
1344
|
+
this.#order[this.#order.indexOf(twin)] = refused;
|
|
1345
|
+
for (const [other, value] of this.#declarations) {
|
|
1346
|
+
if (value === twin)
|
|
1347
|
+
this.#declarations.set(other, refused);
|
|
1348
|
+
}
|
|
1349
|
+
this.#declarations.set(key, refused);
|
|
1350
|
+
return name;
|
|
1351
|
+
}
|
|
1352
|
+
this.#declarations.set(key, declaration);
|
|
1353
|
+
this.#order.push(declaration);
|
|
1354
|
+
return name;
|
|
785
1355
|
}
|
|
786
1356
|
/** Every declaration, dependencies first. */
|
|
787
1357
|
declarations() {
|
|
788
1358
|
return this.#order;
|
|
789
1359
|
}
|
|
1360
|
+
/**
|
|
1361
|
+
* **A contract type whose validator validates nothing is `never` too**, whatever refused it: two
|
|
1362
|
+
* shapes under one name, a union this emitter refuses, a model openapi3 publishes split. The wire
|
|
1363
|
+
* assertion pairs the two by name, and a contract type stating a shape beside `z.never()` failed
|
|
1364
|
+
* it - `interface HBase { name: string }` beside a refused `hBaseSchema`.
|
|
1365
|
+
*/
|
|
1366
|
+
refuse(names) {
|
|
1367
|
+
for (const [index, declaration] of this.#order.entries()) {
|
|
1368
|
+
if (!names.has(declaration.name) || declaration.source === "never")
|
|
1369
|
+
continue;
|
|
1370
|
+
const refused = { ...declaration, source: "never", isObject: false };
|
|
1371
|
+
this.#order[index] = refused;
|
|
1372
|
+
for (const [key, value] of this.#declarations) {
|
|
1373
|
+
if (value === declaration)
|
|
1374
|
+
this.#declarations.set(key, refused);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
790
1378
|
}
|