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.
@@ -0,0 +1,798 @@
1
+ import { getDiscriminatedUnion, getDiscriminatedUnionFromInheritance, getDiscriminator, getTypeName, isGlobalNamespace, isNeverType, isVoidType, navigateTypesInNamespace, } from "@typespec/compiler";
2
+ import { getHttpOperation, getHttpService, getVisibilitySuffix, isOrExtendsHttpFile, isOverloadSameEndpoint, resolveAuthentication, resolveRequestVisibility, Visibility, } from "@typespec/http";
3
+ import { shouldInline } from "@typespec/openapi";
4
+ import { firstReportOf } from "./constraints.js";
5
+ import { reportDiagnostic } from "./lib.js";
6
+ import { includeDerivedModel, externalReferenceOf, holdsRawBytes, bindingIndexerOf, isArrayLiteral, isAssignable, isNamedList, isSpeakableInstance, rawPropertyKey, metadataInfoOf, renderedVisibilityOf, rootVisibilityOf, } from "./zod.js";
7
+ let view;
8
+ const views = new WeakMap();
9
+ export function withServiceView(program, service, run) {
10
+ const restore = beginServiceView(program, service);
11
+ try {
12
+ return run();
13
+ }
14
+ finally {
15
+ restore();
16
+ }
17
+ }
18
+ /** The same scope, held across an `await` - returns what puts the previous one back. */
19
+ export function beginServiceView(program, service) {
20
+ const previous = view;
21
+ let next = views.get(service);
22
+ if (next === undefined) {
23
+ const { usage, reachable } = usageOf(program, service);
24
+ const { claims, unreached, parameters, routeParameters, sharedOptional, requests } = publicationOf(program, service, reachable);
25
+ next = {
26
+ usage,
27
+ publication: claims,
28
+ unreached,
29
+ parameters,
30
+ routeParameters,
31
+ sharedOptional,
32
+ requests,
33
+ };
34
+ views.set(service, next);
35
+ }
36
+ view = next;
37
+ return () => {
38
+ view = previous;
39
+ };
40
+ }
41
+ /**
42
+ * **The types of the service no operation reaches, which openapi3 publishes as components all the
43
+ * same** - its `processUnreferencedSchemas`, with `omitUnreachableTypes` unset: every model, enum and
44
+ * union declared in the service namespace and below it (only the service's own, where the service is
45
+ * the global namespace), but not one an operation reaches, a model whose properties were spread as
46
+ * parameters (published as parameters), a type openapi3 inlines, an HTTP file, or an authentication
47
+ * scheme's model. Rendered at `All` by the caller, as openapi3 renders them. A scalar is validated
48
+ * inline wherever it is used, reached or not, so an unreached one has nothing to declare.
49
+ */
50
+ export function unreachedTypes() {
51
+ return view?.unreached ?? [];
52
+ }
53
+ /** Every type the service publishes, reached by an operation or not - read as {@link usageOf} reads it. */
54
+ export function publishedTypes() {
55
+ return view?.usage.keys() ?? [];
56
+ }
57
+ function unreachedOf(program, service, reachable, parameterModels) {
58
+ const [http] = getHttpService(program, service);
59
+ const schemes = new Set(resolveAuthentication(http).schemes.map((scheme) => scheme.model));
60
+ const found = [];
61
+ const add = (type) => {
62
+ if (type.kind === "Model" &&
63
+ (isOrExtendsHttpFile(program, type) || parameterModels.has(type))) {
64
+ return;
65
+ }
66
+ if (schemes.has(type) || reachable.has(type) || shouldInline(program, type))
67
+ return;
68
+ // Published as a reference to another document, never as a component.
69
+ if (externalReferenceOf(type) !== undefined)
70
+ return;
71
+ found.push(type);
72
+ };
73
+ navigateTypesInNamespace(service, { model: add, enum: add, union: add }, { skipSubNamespaces: isGlobalNamespace(program, service) });
74
+ return found;
75
+ }
76
+ /** Whether openapi3 suffixes this type's component with the visibility it is rendered at. */
77
+ export function isSuffixedByUsage(type) {
78
+ // With no service's usage in scope, every view is told apart, so no two share a name.
79
+ if (view === undefined)
80
+ return true;
81
+ return (view.usage.get(type)?.size ?? 0) > 1;
82
+ }
83
+ function usageOf(program, service) {
84
+ const usage = new Map();
85
+ const exact = (type, at) => {
86
+ const seen = usage.get(type) ?? new Set();
87
+ seen.add(at);
88
+ usage.set(type, seen);
89
+ };
90
+ /**
91
+ * Its effective payload type too, as openapi3's `trackUsage` counts it - but not in the pass over
92
+ * types no operation reaches, which is its `trackUsageExact`: counted there, `{ ...Foo; @header h }`
93
+ * held by an unreached model gave `Foo` a second visibility, and a suffix the document does not.
94
+ */
95
+ let exactOnly = false;
96
+ const track = (type, at) => {
97
+ exact(type, at);
98
+ if (exactOnly)
99
+ return;
100
+ const effective = metadataInfoOf(program).getEffectivePayloadType(type, at);
101
+ if (effective !== type)
102
+ exact(effective, at);
103
+ };
104
+ const visited = new Map();
105
+ const navigate = (type, at) => {
106
+ const seen = visited.get(type) ?? new Set();
107
+ if (seen.has(at))
108
+ return;
109
+ seen.add(at);
110
+ visited.set(type, seen);
111
+ switch (type.kind) {
112
+ case "Model":
113
+ track(type, at);
114
+ for (const property of type.properties.values())
115
+ navigate(property, at);
116
+ if (type.baseModel !== undefined)
117
+ navigate(type.baseModel, at);
118
+ for (const derived of type.derivedModels)
119
+ navigate(derived, at);
120
+ if (type.indexer !== undefined) {
121
+ navigate(type.indexer.value, type.indexer.key.name === "integer" ? at | Visibility.Item : at);
122
+ }
123
+ return;
124
+ case "ModelProperty":
125
+ return navigate(type.type, at);
126
+ case "Union":
127
+ track(type, at);
128
+ for (const variant of type.variants.values())
129
+ navigate(variant, at);
130
+ return;
131
+ case "UnionVariant":
132
+ return navigate(type.type, at);
133
+ default:
134
+ track(type, at);
135
+ }
136
+ };
137
+ const operation = (each) => {
138
+ const [http] = getHttpOperation(program, each);
139
+ const at = resolveRequestVisibility(program, each, http.verb);
140
+ const body = http.parameters.body;
141
+ if (body !== undefined) {
142
+ navigate(body.type, at);
143
+ if (body.bodyKind === "multipart") {
144
+ for (const part of body.parts)
145
+ navigate(part.body.type, at);
146
+ }
147
+ }
148
+ for (const parameter of http.parameters.parameters)
149
+ navigate(parameter.param, at);
150
+ navigate(each.returnType, Visibility.Read);
151
+ };
152
+ const container = (namespace) => {
153
+ for (const nested of namespace.namespaces.values())
154
+ container(nested);
155
+ for (const contract of namespace.interfaces.values()) {
156
+ for (const each of contract.operations.values())
157
+ operation(each);
158
+ }
159
+ for (const each of namespace.operations.values())
160
+ operation(each);
161
+ };
162
+ container(service);
163
+ // openapi3's `isUnreachable`: what no operation reaches, before the pass below adds it at `All`.
164
+ const reachable = new Set(usage.keys());
165
+ exactOnly = true;
166
+ const unreached = (type) => {
167
+ if (!usage.has(type)) {
168
+ visited.clear();
169
+ navigate(type, Visibility.All);
170
+ }
171
+ };
172
+ navigateTypesInNamespace(service, {
173
+ model: unreached,
174
+ scalar: unreached,
175
+ enum: unreached,
176
+ union: unreached,
177
+ });
178
+ return { usage, reachable };
179
+ }
180
+ function isParameterProperty(each) {
181
+ return (each.kind === "header" ||
182
+ each.kind === "query" ||
183
+ each.kind === "path" ||
184
+ each.kind === "cookie");
185
+ }
186
+ function publicationOf(program, service, reachable) {
187
+ const info = metadataInfoOf(program);
188
+ const claims = new Map();
189
+ let completed = 0;
190
+ /** The union of the multipart part being written, where it holds raw `bytes`. */
191
+ let binaryPart;
192
+ /** Run `children` as the emission of `type` reached at `at`, unless openapi3 has it cached. */
193
+ const emitting = (type, at, ignore, via, children) => {
194
+ const reduced = renderedVisibilityOf(program, type, at);
195
+ const byVisibility = claims.get(type) ?? new Map();
196
+ claims.set(type, byVisibility);
197
+ const list = byVisibility.get(reduced) ?? [];
198
+ byVisibility.set(reduced, list);
199
+ const binary = type === binaryPart;
200
+ if (list.some((claim) => claim.ignore === ignore && claim.binary === binary))
201
+ return;
202
+ const claim = { at, ignore, via, order: Number.POSITIVE_INFINITY, binary };
203
+ list.push(claim);
204
+ children(reduced);
205
+ completed += 1;
206
+ claim.order = completed;
207
+ };
208
+ const emit = (type, at, ignore, via) => {
209
+ switch (type.kind) {
210
+ case "ModelProperty":
211
+ return emit(type.type, at, ignore, via);
212
+ /**
213
+ * `unionVariant`: a reference to a variant - `v: U.a` - is its type's, where it is used. Not
214
+ * followed, `IR` sent in a PATCH through `U.a` was published as `IRUpdate` with nothing here
215
+ * to say so, and its validator was `IR`'s.
216
+ */
217
+ case "UnionVariant":
218
+ return emit(type.type, at, ignore, via);
219
+ case "Tuple":
220
+ for (const value of type.values)
221
+ emit(value, at, ignore, via);
222
+ return;
223
+ case "Model":
224
+ if (isArrayLiteral(program, type) && type.indexer !== undefined) {
225
+ // `arrayLiteralReferenceContext`: the elements at the list's visibility, with `Item`.
226
+ return emit(type.indexer.value, at | Visibility.Item, ignore, via);
227
+ }
228
+ return emitting(type, at, ignore, via, (reduced) => {
229
+ if (isNamedList(type) && type.indexer !== undefined) {
230
+ return emit(type.indexer.value, at, ignore, via);
231
+ }
232
+ for (const property of type.properties.values()) {
233
+ if (isNeverType(property.type))
234
+ continue;
235
+ if (!info.isPayloadProperty(property, at, ignore))
236
+ continue;
237
+ emit(property.type, at, ignore, via);
238
+ }
239
+ if (type.indexer !== undefined && !isNeverType(type.indexer.value)) {
240
+ emit(type.indexer.value, reduced, ignore, via);
241
+ }
242
+ for (const derived of type.derivedModels.filter(includeDerivedModel)) {
243
+ emit(derived, reachable.has(derived) ? reduced : Visibility.All, ignore, via);
244
+ }
245
+ const discriminator = getDiscriminator(program, type);
246
+ if (discriminator !== undefined) {
247
+ const [dispatch] = getDiscriminatedUnionFromInheritance(type, discriminator);
248
+ for (const variant of dispatch.variants.values())
249
+ emit(variant, reduced, ignore, via);
250
+ }
251
+ if (type.baseModel !== undefined)
252
+ emit(type.baseModel, reduced, ignore, via);
253
+ });
254
+ case "Enum":
255
+ // A component wherever it is reached, an encoded list's element included.
256
+ return emitting(type, at, ignore, via, () => undefined);
257
+ case "Union":
258
+ return emitting(type, at, ignore, via, (reduced) => {
259
+ const [discriminated] = getDiscriminatedUnion(program, type);
260
+ if (discriminated !== undefined) {
261
+ for (const variant of discriminated.variants.values())
262
+ emit(variant, reduced, ignore, via);
263
+ // A default variant is published beside the others only where there is no envelope.
264
+ if (discriminated.defaultVariant !== undefined &&
265
+ discriminated.options.envelope === "none") {
266
+ emit(discriminated.defaultVariant, reduced, ignore, via);
267
+ }
268
+ return;
269
+ }
270
+ for (const variant of type.variants.values())
271
+ emit(variant.type, reduced, ignore, via);
272
+ });
273
+ default:
274
+ return;
275
+ }
276
+ };
277
+ /** `emitTypeWithSchemaEmitter`: a root not transformed at its visibility is emitted at `Read`. */
278
+ const root = (type, at, ignore, where) => {
279
+ const named = (type.kind === "Model" || type.kind === "Union") && !shouldInline(program, type);
280
+ const name = named ? getTypeName(type, { namespaceFilter: () => false }) : undefined;
281
+ emit(type, rootVisibilityOf(program, type, at), ignore, {
282
+ where,
283
+ root: name,
284
+ model: type.kind === "Model",
285
+ ...(named ? { type } : {}),
286
+ });
287
+ };
288
+ const payload = (body, at, where) => {
289
+ switch (body.bodyKind) {
290
+ case "single":
291
+ return root(info.getEffectivePayloadType(body.type, at), at, body.isExplicit && body.containsMetadataAnnotations, where);
292
+ case "multipart":
293
+ for (const part of body.parts) {
294
+ const inPart = `the '${part.name ?? ""}' part of ${where}`;
295
+ if (part.body.bodyKind !== "file") {
296
+ binaryPart = holdsRawBytes(program, part.body.type) ? part.body.type : undefined;
297
+ payload(part.body, at, inPart);
298
+ binaryPart = undefined;
299
+ }
300
+ for (const header of part.headers)
301
+ root(header.property.type, at, false, inPart);
302
+ }
303
+ return;
304
+ case "file":
305
+ return;
306
+ }
307
+ };
308
+ /** Spread parameters of a named model, which openapi3 emits once and shares. */
309
+ const sharedParameters = new Map();
310
+ /** A shared route's parameter read as the one openapi3 publishes for its name, by its own property. */
311
+ const routeParameters = new Map();
312
+ const sharedOptional = new Set();
313
+ /** Each operation of a shared route, at the visibility of its first. */
314
+ const requests = new Map();
315
+ /** Their models, which openapi3 publishes as parameters and not again as schemas. */
316
+ const parameterModels = new Set();
317
+ const parameter = (operation, each, at) => {
318
+ if (isNeverType(each.property.type))
319
+ return;
320
+ // Published as a reference to another document, with nothing written for it.
321
+ if (externalReferenceOf(each.property) !== undefined)
322
+ return;
323
+ const source = sourceOf(each.property);
324
+ if (sharedParameters.has(source))
325
+ return;
326
+ const where = `the '${each.options.name}' parameter of '${operation.operation.name}'`;
327
+ root(each.property.type, at, false, where);
328
+ if (source !== each.property &&
329
+ source.model !== undefined &&
330
+ !shouldInline(program, source.model)) {
331
+ sharedParameters.set(source, at);
332
+ parameterModels.add(source.model);
333
+ }
334
+ };
335
+ const responses = (operation) => {
336
+ const where = `a response of '${operation.operation.name}'`;
337
+ for (const response of operation.responses) {
338
+ // A response whose type is a reference to another document is that reference, and no more.
339
+ if (externalReferenceOf(response.type) !== undefined)
340
+ continue;
341
+ for (const content of response.responses) {
342
+ for (const header of Object.values(content.headers ?? {})) {
343
+ root(header.type, Visibility.Read, false, where);
344
+ }
345
+ if (content.body !== undefined && !isVoidType(content.body.type)) {
346
+ payload(content.body, Visibility.Read, where);
347
+ }
348
+ }
349
+ }
350
+ };
351
+ const body = (operation, at) => {
352
+ const requestBody = operation.parameters.body;
353
+ if (requestBody === undefined || isVoidType(requestBody.type))
354
+ return;
355
+ payload(requestBody, at, `the request body of '${operation.operation.name}'`);
356
+ };
357
+ const [http] = getHttpService(program, service);
358
+ const routes = new Map();
359
+ for (const operation of http.operations) {
360
+ const overloading = operation.overloading;
361
+ if (overloading !== undefined && isOverloadSameEndpoint({ ...operation, overloading }))
362
+ continue;
363
+ const key = `${operation.verb}|${operation.path}`;
364
+ const shared = routes.get(key);
365
+ if (shared === undefined)
366
+ routes.set(key, [operation]);
367
+ else
368
+ shared.push(operation);
369
+ }
370
+ for (const operations of routes.values()) {
371
+ const [first] = operations;
372
+ if (first === undefined)
373
+ continue;
374
+ const at = resolveRequestVisibility(program, first.operation, first.verb);
375
+ if (operations.length === 1) {
376
+ // `path-query`: openapi3 publishes nothing for an operation whose path holds a query string.
377
+ if (first.path.indexOf("?") > 0)
378
+ continue;
379
+ for (const each of first.parameters.properties) {
380
+ if (isParameterProperty(each))
381
+ parameter(first, each, at);
382
+ }
383
+ responses(first);
384
+ body(first, at);
385
+ continue;
386
+ }
387
+ for (const operation of operations)
388
+ requests.set(operation.operation, at);
389
+ for (const operation of operations)
390
+ responses(operation);
391
+ // `resolveSharedRouteParameters`: by name, in the order each name is first met.
392
+ const byName = new Map();
393
+ for (const operation of operations) {
394
+ for (const each of operation.parameters.properties) {
395
+ if (!isParameterProperty(each))
396
+ continue;
397
+ const group = byName.get(each.options.name) ?? [];
398
+ group.push([operation, each]);
399
+ byName.set(each.options.name, group);
400
+ }
401
+ }
402
+ /**
403
+ * **One parameter per name where every operation declares it alike**, as openapi3 publishes
404
+ * it: the first's, with the location, optionality and type the rest share. The rest are read as
405
+ * that one is; their models are not parameter models, and a spread model only they use is
406
+ * published as a type no operation reaches. Followed member by member, a GET's spread
407
+ * `...CommonF` claimed `CommonF.f` at `Query`, and a later POST sending it was read there,
408
+ * where the document reads it at `Create`.
409
+ */
410
+ for (const group of byName.values()) {
411
+ const [reference, ...rest] = group;
412
+ if (reference === undefined)
413
+ continue;
414
+ const declared = reference[1].property;
415
+ const alike = rest.every(([, each]) => each.kind === reference[1].kind &&
416
+ each.property.optional === declared.optional &&
417
+ each.property.type.kind === declared.type.kind &&
418
+ each.property.type === declared.type);
419
+ /**
420
+ * **Optional where openapi3 makes it so**: a parameter alike in the operations declaring
421
+ * it but missing from one, or in all of them but declared unalike. openapi3 does it by setting
422
+ * `optional` on the declarations while it emits, so read from them, `sa`'s required `q`
423
+ * beside `sb`, which has none, was required when this emitter ran first and optional when it
424
+ * ran after - the document's - from one spec.
425
+ */
426
+ const inAll = group.length === operations.length;
427
+ const consistent = group.every(([, each]) => each.kind === reference[1].kind &&
428
+ each.property.optional === declared.optional &&
429
+ each.property.type.kind === declared.type.kind);
430
+ if ((alike && !inAll) || (inAll && !consistent)) {
431
+ for (const [, each] of group)
432
+ sharedOptional.add(each.property);
433
+ }
434
+ if (!alike) {
435
+ for (const [operation, each] of group)
436
+ parameter(operation, each, at);
437
+ continue;
438
+ }
439
+ parameter(reference[0], reference[1], at);
440
+ const readAt = sharedParameters.get(sourceOf(declared)) ?? at;
441
+ for (const [, each] of rest)
442
+ routeParameters.set(each.property, readAt);
443
+ }
444
+ const bodies = new Set();
445
+ for (const operation of operations) {
446
+ const requestBody = operation.parameters.body;
447
+ if (requestBody === undefined || bodies.has(requestBody))
448
+ continue;
449
+ bodies.add(requestBody);
450
+ body(operation, at);
451
+ }
452
+ }
453
+ // `processUnreferencedSchemas`: what no operation reaches, at `All`, written last.
454
+ const unreached = unreachedOf(program, service, reachable, parameterModels);
455
+ for (const type of unreached)
456
+ root(type, Visibility.All, false, "a type no operation reaches");
457
+ return {
458
+ claims,
459
+ unreached,
460
+ parameters: sharedParameters,
461
+ routeParameters,
462
+ sharedOptional,
463
+ requests,
464
+ };
465
+ }
466
+ /** The declaration a spread parameter was copied from, or the parameter itself. */
467
+ function sourceOf(property) {
468
+ let source = property;
469
+ while (source.sourceProperty !== undefined)
470
+ source = source.sourceProperty;
471
+ return source;
472
+ }
473
+ /**
474
+ * **The visibility openapi3 reads a model's own properties at, where it publishes the model at
475
+ * `at`: the visibility the reference it writes the schema from arrived at** - see
476
+ * {@link publicationOf}. Where nothing reshapes the model there, the two are one schema - anything
477
+ * else is `split-visibility` - but a type a property refers to is reached at the first, and a list
478
+ * or a union instance is not reduced: `Mid { tags: Tags }` first sent in a create body and also
479
+ * returned is published as `Mid`, its `tags` referring to `TagsCreate`. A component is read as
480
+ * `components.schemas` keeps it, the later of two metadata contexts; a model written out where it
481
+ * is used, as the context `ignore` writes it. Where no reference wrote it, at `at`.
482
+ */
483
+ export function readVisibilityOf(program, model, at, ignore) {
484
+ const claims = view?.publication.get(model)?.get(at) ?? [];
485
+ const claim = shouldInline(program, model)
486
+ ? claims.find((each) => each.ignore === ignore)
487
+ : latestOf(claims);
488
+ return claim?.at ?? at;
489
+ }
490
+ /**
491
+ * **The visibility of the component the document keeps for a type rendered at `at`.** Where the type
492
+ * is published at several visibilities under one name - unsuffixed, as openapi3 names a type it
493
+ * counts at one - `components.schemas` keeps the one written last, and every `$ref` to the name is
494
+ * to it. `K extends P`, written at `All` by the pass over types no operation reaches and at `Read` by
495
+ * `P`'s discriminator mapping, is `K` at `Read` in the document; `G extends K`, written out from
496
+ * `K` at `All`, required a create-only `s` the `K` it refers to leaves out. Elsewhere, `at`.
497
+ */
498
+ export function keptVisibilityOf(type, at) {
499
+ const byVisibility = view?.publication.get(type);
500
+ if (byVisibility === undefined || byVisibility.size < 2 || isSuffixedByUsage(type))
501
+ return at;
502
+ let kept;
503
+ for (const [each, claims] of byVisibility) {
504
+ const latest = latestOf(claims);
505
+ if (latest !== undefined && (kept === undefined || latest.order > kept.order)) {
506
+ kept = { at: each, order: latest.order };
507
+ }
508
+ }
509
+ return kept?.at ?? at;
510
+ }
511
+ function latestOf(claims) {
512
+ return claims.reduce((latest, each) => (latest === undefined || each.order > latest.order ? each : latest), undefined);
513
+ }
514
+ /**
515
+ * **Every component openapi3 writes, at the visibility it publishes it at, in the metadata context
516
+ * `components.schemas` keeps** - declared after the routes, so a component no route renders by name
517
+ * is declared all the same. A model a merge holds is written into the merge here, and openapi3
518
+ * publishes it as a component of its own: `L1 extends L0 { m: B }` and `L2 extends L1 { m: Sub }`
519
+ * publish `B` and `Sub`, which had no validator.
520
+ */
521
+ export function publishedComponents(program) {
522
+ const found = [];
523
+ for (const [type, byVisibility] of view?.publication ?? []) {
524
+ if (type.kind !== "Model" && type.kind !== "Union" && type.kind !== "Enum")
525
+ continue;
526
+ if (shouldInline(program, type) || !isSpeakableInstance(type))
527
+ continue;
528
+ if (externalReferenceOf(type) !== undefined)
529
+ continue;
530
+ for (const [at, claims] of byVisibility) {
531
+ // One component per context it is written in: a part's binary union is one of its own.
532
+ for (const binary of [false, true]) {
533
+ const latest = latestOf(claims.filter((claim) => claim.binary === binary));
534
+ if (latest !== undefined)
535
+ found.push({ type, at, ignore: latest.ignore, binary });
536
+ }
537
+ }
538
+ }
539
+ return found;
540
+ }
541
+ /**
542
+ * **The visibility openapi3 publishes a parameter at, where it is not the operation's.** A parameter
543
+ * spread from a named model is published once, as a shared parameter component, from the first
544
+ * operation that sends it, and every later one refers to it: `...CommonA` sent by a GET and then by a
545
+ * POST is read at `Query` for both. Read at each operation's own, the POST was held to a create view
546
+ * the document does not publish for it.
547
+ */
548
+ /**
549
+ * Whether openapi3 publishes a shared route's parameter optional though it is declared required -
550
+ * see `resolveSharedRouteParameters` - asked of the declaration, before or after openapi3 sets its
551
+ * `optional`, with one answer.
552
+ */
553
+ export function isSharedRouteOptional(property) {
554
+ return view?.sharedOptional.has(property) ?? false;
555
+ }
556
+ export function parameterVisibilityOf(property) {
557
+ return view?.routeParameters.get(property) ?? view?.parameters.get(sourceOf(property));
558
+ }
559
+ /**
560
+ * **The visibility openapi3 publishes an operation's request at.** Its own, unless it shares its
561
+ * verb and path with another: openapi3 publishes one operation for the route, every body and
562
+ * parameter at the first one's visibility, and reports `inconsistent-shared-route-request-visibility`
563
+ * where they differ - graded against what it publishes, as the maintainer decided.
564
+ */
565
+ export function requestVisibilityOf(program, operation) {
566
+ return (view?.requests.get(operation.operation) ??
567
+ resolveRequestVisibility(program, operation.operation, operation.verb));
568
+ }
569
+ function contentMisreadingsOf(program, type, claim, rendered) {
570
+ if (isNamedList(type))
571
+ return [];
572
+ const { at: document, ignore, via } = claim;
573
+ const info = metadataInfoOf(program);
574
+ const documentAt = renderedVisibilityOf(program, type, document);
575
+ const renderedAt = renderedVisibilityOf(program, type, rendered);
576
+ const found = [];
577
+ for (const property of type.properties.values()) {
578
+ if (isNeverType(property.type))
579
+ continue;
580
+ const declared = info.isPayloadProperty(property, document, ignore);
581
+ const kept = info.isPayloadProperty(property, renderedAt, ignore);
582
+ const required = info.isPayloadProperty(property, documentAt, ignore) &&
583
+ !info.isOptional(property, documentAt);
584
+ const expected = kept && !info.isOptional(property, renderedAt);
585
+ if (declared !== kept || required !== expected) {
586
+ const effect = !declared
587
+ ? required
588
+ ? "undeclared"
589
+ : "dropped"
590
+ : !kept
591
+ ? "added"
592
+ : "reshaped";
593
+ /**
594
+ * **A key the published schema declares some other way is no misreading**, whether openapi3's
595
+ * `required` names it or not: a dropped optional key a base or an indexer declares is
596
+ * accepted where sent, as the document accepts it.
597
+ */
598
+ if ((effect === "undeclared" || effect === "dropped") &&
599
+ declaredElsewhere(program, type, property, rendered, ignore)) {
600
+ continue;
601
+ }
602
+ found.push({ model: type, property, at: document, effect, via });
603
+ }
604
+ }
605
+ /**
606
+ * **A base's schema is part of every subtype's, through `allOf`.** The subtypes here are rendered
607
+ * with the base's properties written in, not as a reference to the base's declaration, so a base
608
+ * refused for the schema openapi3 publishes for it left each subtype validating what the document
609
+ * refuses: `DerivedK` accepted `{id, name}` while its `allOf` held it to a `BaseK` requiring an
610
+ * `id` it does not declare.
611
+ */
612
+ const base = type.baseModel;
613
+ const inherited = base === undefined
614
+ ? []
615
+ : publishedMisreadingsOf(program, base, renderedVisibilityOf(program, base, documentAt));
616
+ /**
617
+ * **Unless this model declares the key itself**: `D extends B { id: string }` under a `B` whose
618
+ * `required` names an `id` its properties leave out declares it in its own properties, beside the
619
+ * `allOf`, and the document holds `D` as declared. Refused for `B`'s schema, `D` refused every
620
+ * value. Likewise under a `B` that drops an optional `id?` its subtype declares.
621
+ *
622
+ * **Each of the base's misreadings, not its first**: `S extends B { k: string }` under a `B`
623
+ * whose `required` names a `k` and a `bid` its properties leave out declares `k` and not `bid`.
624
+ * Judged by `k` alone, `S`'s create view was validated as declared, requiring a `bid` that its
625
+ * sealed `allOf` refuses - accepting what the document refuses.
626
+ */
627
+ const declaresItself = (misreading) => (misreading.effect === "undeclared" || misreading.effect === "dropped") &&
628
+ [...type.properties.values()].some((property) => rawPropertyKey(program, property) === rawPropertyKey(program, misreading.property) &&
629
+ info.isPayloadProperty(property, document, ignore));
630
+ return [...found, ...inherited.filter((misreading) => !declaresItself(misreading))];
631
+ }
632
+ /**
633
+ * **Whether a key a model's `required` names and its own properties leave out is declared all the
634
+ * same**, so the schema holds a value to it after all: the discriminator of the model declaring it,
635
+ * which openapi3 adds to the properties where they lack it; a base's declaration of the key, through
636
+ * `allOf`; and the indexer that holds every key the model does not name, where it admits the type.
637
+ * Each read as missing, a component the document publishes as declared was refused as
638
+ * `split-visibility`.
639
+ *
640
+ * **A base is judged as its own schema publishes it**, which `allOf` reaches at the visibility the
641
+ * model is reduced to, `rendered`, and openapi3 reads at its own first reference - not at the
642
+ * model's `document`. Read at `Create`, `RBase`'s read-only `id` was missing where the `RBase`
643
+ * the document refers to declares it.
644
+ */
645
+ function declaredElsewhere(program, model, property, rendered, ignore) {
646
+ const key = rawPropertyKey(program, property);
647
+ if (getDiscriminator(program, model)?.propertyName === key)
648
+ return true;
649
+ const info = metadataInfoOf(program);
650
+ for (let link = model.baseModel; link !== undefined; link = link.baseModel) {
651
+ const at = readVisibilityOf(program, link, renderedVisibilityOf(program, link, rendered), ignore);
652
+ for (const each of link.properties.values()) {
653
+ if (rawPropertyKey(program, each) === key && info.isPayloadProperty(each, at, ignore)) {
654
+ return true;
655
+ }
656
+ }
657
+ }
658
+ const binding = bindingIndexerOf(model);
659
+ return (binding !== undefined &&
660
+ !isNeverType(binding.value) &&
661
+ isAssignable(program, property.type, binding.value));
662
+ }
663
+ /**
664
+ * How the schema openapi3 publishes for `type` at `at` differs from that type's schema there, judged
665
+ * against the reference it was written from - see {@link publicationOf}. A component is judged by the
666
+ * one `components.schemas` keeps, the later of the two metadata contexts; a model expression by the
667
+ * one written out in the context it is rendered in, `ignore`. One no reference wrote is still held to
668
+ * its bases'.
669
+ */
670
+ function publishedMisreadingOf(program, type, at, ignore) {
671
+ return publishedMisreadingsOf(program, type, at, ignore)[0];
672
+ }
673
+ /** Every way the schema openapi3 publishes differs - see {@link publishedMisreadingOf}. */
674
+ function publishedMisreadingsOf(program, type, at, ignore) {
675
+ if (type.kind !== "Model")
676
+ return [];
677
+ const claims = view?.publication.get(type)?.get(at) ?? [];
678
+ const claim = ignore === undefined ? latestOf(claims) : claims.find((each) => each.ignore === ignore);
679
+ if (claim !== undefined)
680
+ return contentMisreadingsOf(program, type, claim, at);
681
+ const base = type.baseModel;
682
+ return base === undefined
683
+ ? []
684
+ : publishedMisreadingsOf(program, base, renderedVisibilityOf(program, base, at));
685
+ }
686
+ /**
687
+ * **Whether the schema rendered for `type` here is one openapi3 publishes differently - and if so,
688
+ * `split-visibility`, an error, reported once per declaration.** Refused, as the maintainer decided,
689
+ * rather than validated against a schema the document does not hold: the document's `required` names
690
+ * a property its properties leave out, which no value satisfies under `seal-object-schemas`, or it
691
+ * leaves out an optional one, which a sealed schema then refuses.
692
+ *
693
+ * `declared` is the component's name, for a type published as a component; `type` is then its
694
+ * canonical type and `at` the visibility it is declared at. Otherwise `type` is written out where it
695
+ * is used - a model expression, an inlined template instance - and judged by the emission openapi3
696
+ * writes out in the metadata context `ignore`.
697
+ */
698
+ export function isSplitByVisibility(program, type, at, ignore, declared) {
699
+ if (type.kind !== "Model" || isArrayLiteral(program, type))
700
+ return false;
701
+ const misreading = publishedMisreadingOf(program, type, at, declared === undefined ? ignore : undefined);
702
+ if (misreading === undefined)
703
+ return false;
704
+ if (firstReportOf(program, "split-visibility", type, "")) {
705
+ const own = misreading.model === type;
706
+ const name = declared ??
707
+ (type.name === "" ? undefined : getTypeName(type, { namespaceFilter: () => false }));
708
+ const model = misreading.model.name === ""
709
+ ? "this model expression"
710
+ : `'${getTypeName(misreading.model, { namespaceFilter: () => false })}'`;
711
+ reportDiagnostic(program, {
712
+ code: "split-visibility",
713
+ messageId: !own ? "inherited" : declared === undefined ? "inline" : "default",
714
+ target: type,
715
+ format: {
716
+ name: name ?? "this model expression",
717
+ via: misreading.via.where,
718
+ model,
719
+ property: `'${misreading.property.name}'`,
720
+ at: getVisibilitySuffix(misreading.at & Visibility.All, Visibility.Read),
721
+ effect: EFFECTS[misreading.effect],
722
+ remedy: remedyOf(misreading, model, declared === undefined && own),
723
+ },
724
+ });
725
+ }
726
+ return true;
727
+ }
728
+ /**
729
+ * **What makes openapi3 publish the request apart: the request's own view of its type.** Measured:
730
+ * `@body body: Create<Outer>` publishes `CreateOuter` and, through it, `CreateInner` and every model
731
+ * the transform reaches - `extends` included - and leaves the shared `Inner` to the responses. Named
732
+ * on the model only, `inner: Create<Inner>` in a model a response returns too would change the
733
+ * response; on the request's root type, nothing but the request changes. A model expression has no
734
+ * name to wrap, so it is named first.
735
+ */
736
+ function remedyOf(misreading, model, expression) {
737
+ const template = viewTemplateOf(misreading.at);
738
+ const moved = `, or move '${misreading.property.name}' out of ${model}`;
739
+ const its = template === undefined
740
+ ? "a model holding only what that request sees"
741
+ : `its '${template}<...>' view`;
742
+ if (expression)
743
+ return `declare it as a named model and send ${its} in its place${moved}`;
744
+ const { root, where, model: templated, type } = misreading.via;
745
+ if (root !== undefined) {
746
+ // A visibility template takes a model: a union root is sent as a model of its own.
747
+ if (template === undefined || !templated || type === undefined) {
748
+ return `send a model of '${root}' holding only what that request sees as ${where}${moved}`;
749
+ }
750
+ return `${viewRemedyOf(template, type)} and send it as ${where}, through '@body'${moved}`;
751
+ }
752
+ return `send each model in ${where} as ${its}${moved}`;
753
+ }
754
+ /**
755
+ * Whether openapi3 publishes the component of `type` at `at` split - the refusal
756
+ * {@link isSplitByVisibility} reports, asked without reporting it. A dispatch leaves such a subtype
757
+ * out, as it leaves out one no value is: its validator is `z.never()`, which no
758
+ * `z.discriminatedUnion` can hold.
759
+ */
760
+ export function isPublishedSplit(program, type, at) {
761
+ return (type.kind === "Model" &&
762
+ !shouldInline(program, type) &&
763
+ publishedMisreadingOf(program, type, at) !== undefined);
764
+ }
765
+ const EFFECTS = {
766
+ undeclared: "required and not declared",
767
+ dropped: "not declared",
768
+ added: "declared though it is not part of the schema at 'Read'",
769
+ reshaped: "required where it is optional at 'Read', or optional where it is required",
770
+ };
771
+ /** The standard template that is a model's view at a request visibility, where there is one. */
772
+ /**
773
+ * **The view a remedy names, declared so that it works where it is pasted**: a model of its own,
774
+ * `model OuterCreate is Create<Ns.Outer>;`, with the type spelled from the global namespace. Named
775
+ * `Create<Outer>`, the view inherited `Outer`'s `@friendlyName` and collided with it; spelled by the
776
+ * friendly name, or without its namespace from a sub-namespace, it named nothing; and sent through
777
+ * a spread, `...Create<O1>`, it left the body the document publishes as it was.
778
+ */
779
+ export function viewRemedyOf(template, type) {
780
+ const name = "name" in type && typeof type.name === "string" ? type.name : template;
781
+ return `declare 'model ${name}${template} is ${template}<${getTypeName(type)}>;'`;
782
+ }
783
+ export function viewTemplateOf(at) {
784
+ switch (at & Visibility.All) {
785
+ case Visibility.Create:
786
+ return "Create";
787
+ case Visibility.Update:
788
+ return "Update";
789
+ case Visibility.Create | Visibility.Update:
790
+ return "CreateOrUpdate";
791
+ case Visibility.Delete:
792
+ return "Delete";
793
+ case Visibility.Query:
794
+ return "Query";
795
+ default:
796
+ return undefined;
797
+ }
798
+ }