typespec-http-zod 0.31.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 +3 -0
- package/dist/src/api.js +36 -6
- package/dist/src/lib.d.ts +28 -1
- package/dist/src/lib.js +40 -0
- package/dist/src/openapi-versions.d.ts +20 -0
- package/dist/src/openapi-versions.js +31 -0
- package/dist/src/registry.d.ts +3 -1
- package/dist/src/registry.js +39 -12
- package/dist/src/zod.d.ts +50 -1
- package/dist/src/zod.js +296 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,9 @@ And the OpenAPI that [`@typespec/openapi3`](https://typespec.io) generates from
|
|
|
50
50
|
|
|
51
51
|
- **Documentation and validation cannot drift** - both come from one definition, so there is no
|
|
52
52
|
second source to keep in step
|
|
53
|
+
- **Agrees with the OpenAPI version you publish** - 3.0, `@typespec/openapi3`'s default, and 3.1 read
|
|
54
|
+
some schemas differently; the validators follow the `openapi-versions` you give openapi3, and refuse
|
|
55
|
+
at compile what a 3.0 document cannot say. See [OpenAPI versions](docs/guides.md#openapi-versions)
|
|
53
56
|
- **One runtime dependency** - `zod`, and nothing else
|
|
54
57
|
- **No framework** - use the validators in a server, a client, or a test
|
|
55
58
|
- **Types with zero imports** - a contracts module anything can depend on
|
package/dist/src/api.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { emitFile, getDiscriminatedUnion, getDiscriminator, getDoc, getDocData, getEncode, getLocationContext, getSummary, getTypeName, isErrorModel, isNeverType, isVoidType, navigateProgram, NoTarget, resolvePath, } from "@typespec/compiler";
|
|
2
|
-
import { getAuthenticationForOperation, isOrExtendsHttpFile, Visibility, } from "@typespec/http";
|
|
2
|
+
import { getAuthenticationForOperation, getVisibilitySuffix, isOrExtendsHttpFile, Visibility, } from "@typespec/http";
|
|
3
3
|
import { getOperationId, resolveOperationId } from "@typespec/openapi";
|
|
4
4
|
import { firstReportOf } from "./constraints.js";
|
|
5
5
|
import { reportDiagnostic } from "./lib.js";
|
|
6
|
+
import { openApiVersionsOf, publishesOpenApi30AndLater } from "./openapi-versions.js";
|
|
6
7
|
import { literal } from "./literal.js";
|
|
7
8
|
import { beginServiceView, parameterVisibilityOf, publishedComponents, publishedTypes, requestVisibilityOf, unreachedTypes, withServiceView, } from "./published.js";
|
|
8
9
|
import { isSingleObjectLiteral, SchemaRegistry, TypeRegistry } from "./registry.js";
|
|
@@ -13,7 +14,7 @@ import { resolveVersioningModule, serviceSnapshots } from "./versioning.js";
|
|
|
13
14
|
import { EXPLODED_QUERY_SOURCE, explodedQueryCall, eachReader, NO_READERS, needsExpansionDecoding, routeTemplateOf, URI_EXPANSION_SOURCE, uriExpansionCall, WIRE_READ_OBJECT_SOURCE, WIRE_READ_AS_SOURCE, WIRE_READ_SOURCE, WIRE_ITSELF_SOURCE, WIRE_READERS_SOURCE, wireItselfCall, wireReadObjectCall, } from "./uri-template.js";
|
|
14
15
|
export { CONTENTLESS_STATUS_CODES, STATUS_CODES } from "./statuses.js";
|
|
15
16
|
export { isJsonMediaType, isMediaRange, isTextualMediaType, mediaTypeEssenceOf, mediaTypeTypeOf, resultMembersOf, statusCodesOf, } from "./results.js";
|
|
16
|
-
import { collectExternalImports, encodedTypeOf, externalReferenceOf, holdsRawBytes, isExclusive, isTypelessMember, nullableInnerOf, noteDeclaredVocabularies, noteKeyVocabularies, objectKey, objectOfModel, reachesItselfThroughUnions, refuseExternalReference, refuseSelfReachingUnion, atItem, bindingIndexerOf, currentVisibility, dispatchedSubtypesOf, readsFromText, renderedVisibilityOf, rootVisibilityOf, withContractsPackage, withSealedObjects, withMetadataAnnotationsIgnored, withMultipartBinary, withParameterPosition, UNREPRESENTABLE, withVisibility, } from "./zod.js";
|
|
17
|
+
import { collectExternalImports, encodedTypeOf, externalReferenceOf, holdsRawBytes, isExclusive, isTypelessMember, nullableInnerOf, noteDeclaredVocabularies, noteKeyVocabularies, objectKey, objectOfModel, reachesItselfThroughUnions, refuseExternalReference, refuseSelfReachingUnion, atItem, bindingIndexerOf, currentVisibility, dispatchedSubtypesOf, readsFromText, renderedVisibilityOf, rootVisibilityOf, withContractsPackage, withSealedObjects, withMetadataAnnotationsIgnored, requiredReadOnlyIn, reportUnpublishable, withRequestReading, withMultipartBinary, withParameterPosition, UNREPRESENTABLE, withVisibility, } from "./zod.js";
|
|
17
18
|
/**
|
|
18
19
|
* `typespec-http-zod` - Zod validators and framework-free contract types from a TypeSpec HTTP
|
|
19
20
|
* service, agreeing with the OpenAPI document `@typespec/openapi3` publishes from the same source.
|
|
@@ -626,6 +627,33 @@ function bodyTypeOf(type) {
|
|
|
626
627
|
* `undeclared-discriminator` refuses a discriminator the document cannot state. `void` says "no
|
|
627
628
|
* body", and openapi3 publishes it as none.
|
|
628
629
|
*/
|
|
630
|
+
/**
|
|
631
|
+
* **A request holding a required read-only property, where openapi3 publishes 3.0 and a later
|
|
632
|
+
* version** - see `ambiguous-read-only`. The 3.0 document lets the request leave it out and the later
|
|
633
|
+
* one does not, so no request validator agrees with both.
|
|
634
|
+
*/
|
|
635
|
+
function reportAmbiguousReadOnly(program, type, requested) {
|
|
636
|
+
if (!publishesOpenApi30AndLater(program))
|
|
637
|
+
return;
|
|
638
|
+
const property = requiredReadOnlyIn(program, type, rootVisibilityOf(program, type, requested), new Set());
|
|
639
|
+
if (property === undefined || !firstReportOf(program, "ambiguous-read-only", type))
|
|
640
|
+
return;
|
|
641
|
+
reportDiagnostic(program, {
|
|
642
|
+
code: "ambiguous-read-only",
|
|
643
|
+
target: type,
|
|
644
|
+
format: {
|
|
645
|
+
type: getTypeName(type),
|
|
646
|
+
property: property.model === undefined
|
|
647
|
+
? property.name
|
|
648
|
+
: `${getTypeName(property.model)}.${property.name}`,
|
|
649
|
+
later: openApiVersionsOf(program)
|
|
650
|
+
.filter((version) => !version.startsWith("3.0."))
|
|
651
|
+
.map((version) => `OpenAPI ${version}`)
|
|
652
|
+
.join(" and "),
|
|
653
|
+
view: `${getVisibilitySuffix(requested, Visibility.Read)}<${getTypeName(property.model ?? type)}>`,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
629
657
|
function reportNeverBody(program, operation, body, position) {
|
|
630
658
|
if (body?.bodyKind !== "single" || !isNeverType(body.type))
|
|
631
659
|
return;
|
|
@@ -2490,6 +2518,7 @@ export function collectRoutes(program, registry, service) {
|
|
|
2490
2518
|
}
|
|
2491
2519
|
function collectServiceRoutes(program, registry, service) {
|
|
2492
2520
|
const routes = [];
|
|
2521
|
+
reportUnpublishable(program, service.namespace);
|
|
2493
2522
|
{
|
|
2494
2523
|
for (const operation of service.operations) {
|
|
2495
2524
|
/**
|
|
@@ -2583,7 +2612,7 @@ function collectServiceRoutes(program, registry, service) {
|
|
|
2583
2612
|
* one and not the other, and `@visibility` can add or remove properties outright. Walking
|
|
2584
2613
|
* both at one visibility gives whichever was seen first to both.
|
|
2585
2614
|
*/
|
|
2586
|
-
...withVisibility(requestVisibility, () => {
|
|
2615
|
+
...withVisibility(requestVisibility, () => withRequestReading(program, requestVisibility, () => {
|
|
2587
2616
|
// Multipart first: its body has no single type to walk, and skipping it left every
|
|
2588
2617
|
// such route mounted with nothing checking the request at all.
|
|
2589
2618
|
const parameterBody = requestBodyParameterOf(operation);
|
|
@@ -2595,6 +2624,7 @@ function collectServiceRoutes(program, registry, service) {
|
|
|
2595
2624
|
}
|
|
2596
2625
|
if (requestType === undefined)
|
|
2597
2626
|
return { requestSchema: undefined };
|
|
2627
|
+
reportAmbiguousReadOnly(program, requestType, requestVisibility);
|
|
2598
2628
|
const declared = withMetadataAnnotationsIgnored(ignoresMetadata(parameterBody), () => registry.expressionFor(requestType, true));
|
|
2599
2629
|
const form = formBodyOf(program, registry, operation, parameterBody, requestType, declared);
|
|
2600
2630
|
return form === undefined
|
|
@@ -2602,7 +2632,7 @@ function collectServiceRoutes(program, registry, service) {
|
|
|
2602
2632
|
: form.only
|
|
2603
2633
|
? { requestSchema: form.schema }
|
|
2604
2634
|
: { requestSchema: declared, formRequestSchema: form.schema };
|
|
2605
|
-
}),
|
|
2635
|
+
})),
|
|
2606
2636
|
...(() => {
|
|
2607
2637
|
const split = withVisibility(requestVisibility, () => parameterSchemasOf(program, operation, registry, template.segments, template.literalQuery.map(([name]) => name)));
|
|
2608
2638
|
return {
|
|
@@ -3253,7 +3283,7 @@ function collectRequestTypes(program, registry, service) {
|
|
|
3253
3283
|
* The predicates `propertyToTs` consults read the visibility `withVisibility` sets, and outside
|
|
3254
3284
|
* one they read `Read`.
|
|
3255
3285
|
*/
|
|
3256
|
-
withVisibility(requestVisibilityOf(program, operation), () => {
|
|
3286
|
+
withVisibility(requestVisibilityOf(program, operation), () => withRequestReading(program, requestVisibilityOf(program, operation), () => {
|
|
3257
3287
|
const body = requestBodyOf(operation);
|
|
3258
3288
|
const placement = requestBodyPlacementOf(program, operation);
|
|
3259
3289
|
/** The parts of a multipart body, which `requestBodyOf` deliberately does not return. */
|
|
@@ -3351,7 +3381,7 @@ function collectRequestTypes(program, registry, service) {
|
|
|
3351
3381
|
: `Simplify<${parts.join(" & ")}>`,
|
|
3352
3382
|
streamsBody,
|
|
3353
3383
|
});
|
|
3354
|
-
});
|
|
3384
|
+
}));
|
|
3355
3385
|
}
|
|
3356
3386
|
return entries;
|
|
3357
3387
|
}
|
package/dist/src/lib.d.ts
CHANGED
|
@@ -194,6 +194,15 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
194
194
|
"never-body": {
|
|
195
195
|
readonly default: import("@typespec/compiler").CallableMessage<["operationId", "position"]>;
|
|
196
196
|
};
|
|
197
|
+
"unpublishable-null": {
|
|
198
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "why"]>;
|
|
199
|
+
};
|
|
200
|
+
"unpublishable-record": {
|
|
201
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "base", "base"]>;
|
|
202
|
+
};
|
|
203
|
+
"ambiguous-read-only": {
|
|
204
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "property", "later", "view"]>;
|
|
205
|
+
};
|
|
197
206
|
"unsatisfiable-encoding": {
|
|
198
207
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "declared", "encoded", "why"]>;
|
|
199
208
|
};
|
|
@@ -225,7 +234,7 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
225
234
|
readonly context: import("@typespec/compiler").CallableMessage<["name"]>;
|
|
226
235
|
};
|
|
227
236
|
}, EmitterOptions, never>;
|
|
228
|
-
export declare const reportDiagnostic: <C extends "ambiguous-form-reading" | "default-on-required-property" | "duplicate-declaration" | "duplicate-operation-id" | "empty-union" | "header-case-collision" | "inline-cycle" | "invalid-default" | "invalid-subtype-discriminator" | "never-body" | "non-unicode-pattern" | "optional-prototype-name" | "sealed-base-extended" | "shared-input-key" | "split-visibility" | "truncated-doc-comment" | "undeclared-discriminator" | "unenforceable-encoded-constraint" | "unenforceable-property-name" | "unknown-key-vocabulary" | "unmirrorable-seal" | "unrepresentable-override" | "unsatisfiable-encoding" | "unsupported-default" | "unsupported-status-code-range" | "unsupported-type", M extends keyof {
|
|
237
|
+
export declare const reportDiagnostic: <C extends "ambiguous-form-reading" | "ambiguous-read-only" | "default-on-required-property" | "duplicate-declaration" | "duplicate-operation-id" | "empty-union" | "header-case-collision" | "inline-cycle" | "invalid-default" | "invalid-subtype-discriminator" | "never-body" | "non-unicode-pattern" | "optional-prototype-name" | "sealed-base-extended" | "shared-input-key" | "split-visibility" | "truncated-doc-comment" | "undeclared-discriminator" | "unenforceable-encoded-constraint" | "unenforceable-property-name" | "unknown-key-vocabulary" | "unmirrorable-seal" | "unpublishable-null" | "unpublishable-record" | "unrepresentable-override" | "unsatisfiable-encoding" | "unsupported-default" | "unsupported-status-code-range" | "unsupported-type", M extends keyof {
|
|
229
238
|
"unsupported-type": {
|
|
230
239
|
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why", "remedy"]>;
|
|
231
240
|
};
|
|
@@ -291,6 +300,15 @@ export declare const reportDiagnostic: <C extends "ambiguous-form-reading" | "de
|
|
|
291
300
|
"never-body": {
|
|
292
301
|
readonly default: import("@typespec/compiler").CallableMessage<["operationId", "position"]>;
|
|
293
302
|
};
|
|
303
|
+
"unpublishable-null": {
|
|
304
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "why"]>;
|
|
305
|
+
};
|
|
306
|
+
"unpublishable-record": {
|
|
307
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "base", "base"]>;
|
|
308
|
+
};
|
|
309
|
+
"ambiguous-read-only": {
|
|
310
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "property", "later", "view"]>;
|
|
311
|
+
};
|
|
294
312
|
"unsatisfiable-encoding": {
|
|
295
313
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "declared", "encoded", "why"]>;
|
|
296
314
|
};
|
|
@@ -387,6 +405,15 @@ export declare const reportDiagnostic: <C extends "ambiguous-form-reading" | "de
|
|
|
387
405
|
"never-body": {
|
|
388
406
|
readonly default: import("@typespec/compiler").CallableMessage<["operationId", "position"]>;
|
|
389
407
|
};
|
|
408
|
+
"unpublishable-null": {
|
|
409
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "why"]>;
|
|
410
|
+
};
|
|
411
|
+
"unpublishable-record": {
|
|
412
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "base", "base"]>;
|
|
413
|
+
};
|
|
414
|
+
"ambiguous-read-only": {
|
|
415
|
+
readonly default: import("@typespec/compiler").CallableMessage<["type", "property", "later", "view"]>;
|
|
416
|
+
};
|
|
390
417
|
"unsatisfiable-encoding": {
|
|
391
418
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "declared", "encoded", "why"]>;
|
|
392
419
|
};
|
package/dist/src/lib.js
CHANGED
|
@@ -475,6 +475,46 @@ const diagnostics = {
|
|
|
475
475
|
default: paramMessage `'${"operationId"}' declares a ${"position"} body of type 'never', which has no values, and '@typespec/openapi3' publishes it as any JSON value. Replace it with 'void' to declare no body.`,
|
|
476
476
|
},
|
|
477
477
|
},
|
|
478
|
+
/**
|
|
479
|
+
* **A `null` the OpenAPI 3.0 document cannot publish.** OAS 3.0.4: `nullable` "only takes effect if
|
|
480
|
+
* `type` is explicitly defined within the same Schema Object", and "Other Schema Object constraints
|
|
481
|
+
* retain their defined behavior, and therefore may disallow the use of `null`". `@typespec/openapi3`
|
|
482
|
+
* publishes `Inner | null` as `{ type: object, allOf: [Inner], nullable: true }`, `Color | null` as
|
|
483
|
+
* `{ allOf: [Color], nullable: true }` and `"a" | "b" | null` as `{ type: string, enum: [a, b],
|
|
484
|
+
* nullable: true }`, and each refuses `null` by that text; a bare `null` is `{ nullable: true }`,
|
|
485
|
+
* any value. Refused where openapi3 publishes 3.0, since a validator agreeing with the spec's
|
|
486
|
+
* `| null` disagrees with the document; at 3.1 each is `anyOf` with `{ type: "null" }`.
|
|
487
|
+
*/
|
|
488
|
+
"unpublishable-null": {
|
|
489
|
+
severity: "error",
|
|
490
|
+
messages: {
|
|
491
|
+
default: paramMessage `'${"type"}' admits null, and the OpenAPI 3.0 document '@typespec/openapi3' publishes does not: ${"why"}. Set 'openapi-versions: ["3.1.0"]' under '@typespec/openapi3' in tspconfig.yaml's options, whether or not it is emitted, to publish OpenAPI 3.1, which says null.`,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
/**
|
|
495
|
+
* **A sealed model whose base is a record, where openapi3 publishes 3.0.** openapi3's 3.0 sealing
|
|
496
|
+
* is `additionalProperties: { not: {} }` beside the model's own `properties`, which cannot see the
|
|
497
|
+
* record its base declares in `allOf`, so the document refuses every key the record admits; 3.1
|
|
498
|
+
* seals with `unevaluatedProperties`, which can.
|
|
499
|
+
*/
|
|
500
|
+
"unpublishable-record": {
|
|
501
|
+
severity: "error",
|
|
502
|
+
messages: {
|
|
503
|
+
default: paramMessage `'${"type"}' extends the record '${"base"}' and is sealed, and the OpenAPI 3.0 document '@typespec/openapi3' publishes seals it with 'additionalProperties: { not: {} }', which does not see the record in its 'allOf': every key the record admits is refused. Set 'openapi-versions: ["3.1.0"]' under '@typespec/openapi3', which seals with 'unevaluatedProperties', or spread the record onto the model, '...${"base"}', which is published open.`,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
/**
|
|
507
|
+
* **A required read-only property in a request, where openapi3 publishes 3.0 and a later version.**
|
|
508
|
+
* OAS 3.0.4 says such a `required` "will take effect on the response only"; 3.1 made `readOnly`
|
|
509
|
+
* an annotation, so `required` holds in a request too. One request validator cannot agree with both
|
|
510
|
+
* documents.
|
|
511
|
+
*/
|
|
512
|
+
"ambiguous-read-only": {
|
|
513
|
+
severity: "error",
|
|
514
|
+
messages: {
|
|
515
|
+
default: paramMessage `'${"type"}' is sent in a request and holds the required read-only property '${"property"}', and '@typespec/openapi3' publishes OpenAPI 3.0, where a request may leave it out, and ${"later"}, where it must send it: a request validator agrees with only one. Set one version in 'openapi-versions', or declare the request's own view of it, '${"view"}', which openapi3 publishes without it.`,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
478
518
|
/**
|
|
479
519
|
* **An `@encode` whose published schema no value satisfies, or which states no type.**
|
|
480
520
|
*
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Program } from "@typespec/compiler";
|
|
2
|
+
/**
|
|
3
|
+
* **The OpenAPI versions `@typespec/openapi3` publishes from this compile**: its `openapi-versions`
|
|
4
|
+
* option, read from the compile's options as `typespec-openapi-document` reads it, and openapi3's own
|
|
5
|
+
* default, `3.0.0`, where nothing sets it - whether or not openapi3 is emitted, since a project
|
|
6
|
+
* serving no document is still described by the one openapi3 would write.
|
|
7
|
+
*
|
|
8
|
+
* **The versions are two dialects, and a validator agrees with the one published.** 0.31.0 read every
|
|
9
|
+
* schema as OpenAPI 3.1 reads it while openapi3 published 3.0 by default, and the two answer one
|
|
10
|
+
* schema differently in two places: a required read-only property is required "on the response only"
|
|
11
|
+
* at 3.0 and in both directions at 3.1, and 3.0's `nullable` takes effect only beside `type` - see
|
|
12
|
+
* {@link publishesOpenApi30Only} and `unpublishable-null`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function openApiVersionsOf(program: Program): readonly string[];
|
|
15
|
+
/** Whether every document openapi3 publishes here is OpenAPI 3.0, which reads a request as 3.0 does. */
|
|
16
|
+
export declare function publishesOpenApi30Only(program: Program): boolean;
|
|
17
|
+
/** Whether openapi3 publishes an OpenAPI 3.0 document here, beside any other. */
|
|
18
|
+
export declare function publishesOpenApi30(program: Program): boolean;
|
|
19
|
+
/** Whether openapi3 publishes OpenAPI 3.0 and a later version here, two documents a request reads apart. */
|
|
20
|
+
export declare function publishesOpenApi30AndLater(program: Program): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The OpenAPI versions `@typespec/openapi3` publishes from this compile**: its `openapi-versions`
|
|
3
|
+
* option, read from the compile's options as `typespec-openapi-document` reads it, and openapi3's own
|
|
4
|
+
* default, `3.0.0`, where nothing sets it - whether or not openapi3 is emitted, since a project
|
|
5
|
+
* serving no document is still described by the one openapi3 would write.
|
|
6
|
+
*
|
|
7
|
+
* **The versions are two dialects, and a validator agrees with the one published.** 0.31.0 read every
|
|
8
|
+
* schema as OpenAPI 3.1 reads it while openapi3 published 3.0 by default, and the two answer one
|
|
9
|
+
* schema differently in two places: a required read-only property is required "on the response only"
|
|
10
|
+
* at 3.0 and in both directions at 3.1, and 3.0's `nullable` takes effect only beside `type` - see
|
|
11
|
+
* {@link publishesOpenApi30Only} and `unpublishable-null`.
|
|
12
|
+
*/
|
|
13
|
+
export function openApiVersionsOf(program) {
|
|
14
|
+
const options = program.compilerOptions.options?.["@typespec/openapi3"];
|
|
15
|
+
const versions = options?.["openapi-versions"];
|
|
16
|
+
return versions === undefined || versions.length === 0 ? ["3.0.0"] : versions;
|
|
17
|
+
}
|
|
18
|
+
const is30 = (version) => version.startsWith("3.0.");
|
|
19
|
+
/** Whether every document openapi3 publishes here is OpenAPI 3.0, which reads a request as 3.0 does. */
|
|
20
|
+
export function publishesOpenApi30Only(program) {
|
|
21
|
+
return openApiVersionsOf(program).every(is30);
|
|
22
|
+
}
|
|
23
|
+
/** Whether openapi3 publishes an OpenAPI 3.0 document here, beside any other. */
|
|
24
|
+
export function publishesOpenApi30(program) {
|
|
25
|
+
return openApiVersionsOf(program).some(is30);
|
|
26
|
+
}
|
|
27
|
+
/** Whether openapi3 publishes OpenAPI 3.0 and a later version here, two documents a request reads apart. */
|
|
28
|
+
export function publishesOpenApi30AndLater(program) {
|
|
29
|
+
const versions = openApiVersionsOf(program);
|
|
30
|
+
return versions.some(is30) && versions.some((version) => !is30(version));
|
|
31
|
+
}
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ interface Declaration {
|
|
|
62
62
|
/** The registry key it was declared under, and its metadata context, to render it again. */
|
|
63
63
|
readonly key?: string;
|
|
64
64
|
readonly ignore?: boolean;
|
|
65
|
+
/** The 3.0 request it is read for, to render it again - see `withRequestReading`. */
|
|
66
|
+
readonly reading?: Visibility;
|
|
65
67
|
/** A shape no type stands for, and the type it is published for - see `declareNamed`. */
|
|
66
68
|
readonly synthetic?: Synthetic;
|
|
67
69
|
/** The keys a merge in it asked to be refused, to render it again when one is - see `refusedAt`. */
|
|
@@ -72,7 +74,7 @@ interface Synthetic {
|
|
|
72
74
|
readonly kind: "multipart" | "envelope";
|
|
73
75
|
readonly site: Type;
|
|
74
76
|
}
|
|
75
|
-
export declare function nameAt(bare: string, at: Visibility, type: Type): string;
|
|
77
|
+
export declare function nameAt(bare: string, at: Visibility, type: Type, reading?: Visibility): string;
|
|
76
78
|
export declare class SchemaRegistry {
|
|
77
79
|
#private;
|
|
78
80
|
constructor(program: Program);
|
package/dist/src/registry.js
CHANGED
|
@@ -4,7 +4,7 @@ import { firstReportOf } from "./constraints.js";
|
|
|
4
4
|
import { reportDiagnostic } from "./lib.js";
|
|
5
5
|
import { isSplitByVisibility, isSuffixedByUsage, viewRemedyOf, viewTemplateOf, } from "./published.js";
|
|
6
6
|
import { propertyToTs, propertyTypeToTs, typeToTs, typeToTsBody, withTsRefResolver, withTsSyntheticDeclarations, } from "./types.js";
|
|
7
|
-
import { captureBackEdges, currentVisibility, described, externalReferenceOf, isArrayLiteral, isOptionalAt, isSpeakableInstance, refuseExternalReference, metadataAnnotationsIgnored, metadataInfoOf, multipartBinaryContext, noteBackEdge, OWN_POSITION, propertyToZod, renderedVisibilityOf, rootVisibilityOf, UNREPRESENTABLE, typeToZodBody, visibilityName, withDeclarationSite, withDeferredAnnotator, withMetadataAnnotationsIgnored, withMultipartBinary, withRefResolver, withHeldDeclarations, withSyntheticDeclarations, withParameterPosition, withVisibility, } from "./zod.js";
|
|
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";
|
|
8
8
|
/**
|
|
9
9
|
* The words TypeScript refuses as the name of a declaration the emitted files write - a type alias,
|
|
10
10
|
* an interface, and the references to each - in a module, which is strict mode.
|
|
@@ -429,12 +429,33 @@ function visibilityAt(program, type, requested, depth) {
|
|
|
429
429
|
? rootVisibilityOf(program, type, requested)
|
|
430
430
|
: renderedVisibilityOf(program, type, requested);
|
|
431
431
|
}
|
|
432
|
-
export function nameAt(bare, at, type) {
|
|
432
|
+
export function nameAt(bare, at, type, reading) {
|
|
433
|
+
/**
|
|
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.
|
|
438
|
+
*/
|
|
439
|
+
if (reading !== undefined) {
|
|
440
|
+
return safeDeclarationName(`${bare}${getVisibilitySuffix(reading, Visibility.Read)}`);
|
|
441
|
+
}
|
|
433
442
|
// Suffixed only where openapi3 suffixes it: used at more than one visibility - see `isSuffixedByUsage`.
|
|
434
443
|
const suffixed = at !== Visibility.Read && isSuffixedByUsage(type);
|
|
435
444
|
return safeDeclarationName(`${bare}${suffixed ? getVisibilitySuffix(at, Visibility.Read) : ""}`);
|
|
436
445
|
}
|
|
437
|
-
|
|
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) {
|
|
438
459
|
/**
|
|
439
460
|
* **Keyed on the named model, never a copy of it**: `@typespec/http` synthesises a fresh anonymous
|
|
440
461
|
* model per position, and two operations taking one body each hand over a copy. Both resolve to
|
|
@@ -445,7 +466,9 @@ function keyFor(type, visibility) {
|
|
|
445
466
|
const body = metadataAnnotationsIgnored() ? "\u0000body" : "";
|
|
446
467
|
// And inside a multipart part's union holding raw `bytes` - see `withMultipartBinary`.
|
|
447
468
|
const binary = multipartBinaryContext() ? "\u0000multipart" : "";
|
|
448
|
-
|
|
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}`;
|
|
449
472
|
}
|
|
450
473
|
/** A stable per-type identity, since two models can share a name across namespaces. */
|
|
451
474
|
const identities = new WeakMap();
|
|
@@ -541,7 +564,8 @@ export class SchemaRegistry {
|
|
|
541
564
|
* request and a response. Collapsing onto `Read` first is what makes the key and the name agree,
|
|
542
565
|
* and it is the same collapse openapi3 performs before naming a component.
|
|
543
566
|
*/
|
|
544
|
-
const
|
|
567
|
+
const reading = readingOf(this.#program, type, at);
|
|
568
|
+
const key = keyFor(type, at, reading);
|
|
545
569
|
const existing = this.#declarations.get(key);
|
|
546
570
|
if (existing !== undefined)
|
|
547
571
|
return existing.identifier;
|
|
@@ -566,7 +590,7 @@ export class SchemaRegistry {
|
|
|
566
590
|
this.#cyclic.add(inFlight);
|
|
567
591
|
return pending;
|
|
568
592
|
}
|
|
569
|
-
const name = nameAt(bare, at, type);
|
|
593
|
+
const name = nameAt(bare, at, type, reading);
|
|
570
594
|
const identifier = `${lowerFirst(name)}Schema`;
|
|
571
595
|
const { source, deferred, consulted } = this.#render(type, at, name, key, identifier);
|
|
572
596
|
const declaration = {
|
|
@@ -577,6 +601,7 @@ export class SchemaRegistry {
|
|
|
577
601
|
at,
|
|
578
602
|
key,
|
|
579
603
|
ignore: metadataAnnotationsIgnored(),
|
|
604
|
+
...(reading === undefined ? {} : { reading }),
|
|
580
605
|
...(this.#cyclic.has(key) ? { structural: this.#structuralBodyOf(type, at) } : {}),
|
|
581
606
|
...(deferred ? { annotated: true } : {}),
|
|
582
607
|
...(consulted.size === 0 ? {} : { consulted }),
|
|
@@ -713,14 +738,14 @@ export class SchemaRegistry {
|
|
|
713
738
|
refused: (expression) => this.#refused.has(expression),
|
|
714
739
|
annotated: (expression) => this.#order.some((declaration) => declaration.identifier === expression && declaration.annotated === true),
|
|
715
740
|
refusedAt: (type, at) => {
|
|
716
|
-
const key = keyFor(type, at);
|
|
741
|
+
const key = keyFor(type, at, readingOf(this.#program, type, at));
|
|
717
742
|
this.#consulted.at(-1)?.add(key);
|
|
718
743
|
const declared = this.#declarations.get(key);
|
|
719
744
|
return (declared !== undefined &&
|
|
720
745
|
(this.#refused.has(declared.identifier) || this.#withheld.has(declared.identifier)));
|
|
721
746
|
},
|
|
722
747
|
holdsRefusalAt: (type, at) => {
|
|
723
|
-
const key = keyFor(type, at);
|
|
748
|
+
const key = keyFor(type, at, readingOf(this.#program, type, at));
|
|
724
749
|
this.#consulted.at(-1)?.add(key);
|
|
725
750
|
const declared = this.#declarations.get(key);
|
|
726
751
|
return declared !== undefined && this.#reachesRefusal(declared.identifier, new Set());
|
|
@@ -771,7 +796,7 @@ export class SchemaRegistry {
|
|
|
771
796
|
.map(([each]) => each);
|
|
772
797
|
for (const each of keys)
|
|
773
798
|
this.#declarations.delete(each);
|
|
774
|
-
const { source, deferred, consulted } = withMetadataAnnotationsIgnored(held.ignore ?? false, () => this.#render(type, at, held.typeName, key, held.identifier));
|
|
799
|
+
const { source, deferred, consulted } = withRequestReadingAs(held.reading, () => withMetadataAnnotationsIgnored(held.ignore ?? false, () => this.#render(type, at, held.typeName, key, held.identifier)));
|
|
775
800
|
const fresh = {
|
|
776
801
|
...held,
|
|
777
802
|
source,
|
|
@@ -953,7 +978,8 @@ export class SchemaRegistry {
|
|
|
953
978
|
const bare = declaredNameOf(this.#program, candidate);
|
|
954
979
|
if (bare === undefined)
|
|
955
980
|
return undefined;
|
|
956
|
-
|
|
981
|
+
const at = renderedVisibilityOf(this.#program, candidate, currentVisibility());
|
|
982
|
+
return nameAt(bare, at, candidate, readingOf(this.#program, candidate, at));
|
|
957
983
|
}
|
|
958
984
|
/**
|
|
959
985
|
* Render `type` without consulting its own declaration, but letting nested types use theirs.
|
|
@@ -1154,8 +1180,9 @@ export class TypeRegistry {
|
|
|
1154
1180
|
this.#inlineInProgress.delete(type);
|
|
1155
1181
|
}
|
|
1156
1182
|
}
|
|
1157
|
-
const
|
|
1158
|
-
const
|
|
1183
|
+
const reading = readingOf(this.#program, type, at);
|
|
1184
|
+
const key = keyFor(type, at, reading);
|
|
1185
|
+
const name = nameAt(bare, at, type, reading);
|
|
1159
1186
|
const existing = this.#declarations.get(key);
|
|
1160
1187
|
if (existing !== undefined)
|
|
1161
1188
|
return existing.name;
|
package/dist/src/zod.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Enum, type Model, type ModelProperty, type Program, type Scalar, type Type, type Union } from "@typespec/compiler";
|
|
1
|
+
import { type Enum, type Model, type ModelProperty, type Namespace, type Program, type Scalar, type Type, type Union } from "@typespec/compiler";
|
|
2
2
|
import { type MetadataInfo, Visibility } from "@typespec/http";
|
|
3
3
|
/**
|
|
4
4
|
* What a walk returns after reporting a refusal.
|
|
@@ -27,6 +27,21 @@ export declare function metadataInfoOf(program: Program): MetadataInfo;
|
|
|
27
27
|
export declare function withVisibility<T>(next: Visibility, run: () => T): T;
|
|
28
28
|
/** Whether a property is optional AT THE CURRENT POSITION - PATCH makes update fields optional. */
|
|
29
29
|
export declare function isOptionalAt(program: Program, property: ModelProperty): boolean;
|
|
30
|
+
export declare function withRequestReading<T>(program: Program, requested: Visibility, run: () => T): T;
|
|
31
|
+
/** A declaration's recorded reading, restored to render it again - see `Declaration.reading`. */
|
|
32
|
+
export declare function withRequestReadingAs<T>(reading: Visibility | undefined, run: () => T): T;
|
|
33
|
+
/** The request visibility a 3.0 request reading is under, or `undefined` outside one. */
|
|
34
|
+
export declare function requestReadingAt(): Visibility | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* **Whether a type, rendered at `at`, is read apart in a 3.0 request**: it reaches a required
|
|
37
|
+
* read-only property openapi3 publishes in it - through its own and inherited properties, a list's
|
|
38
|
+
* elements, a record's values, a union's members and a discriminated base's subtypes, each at the
|
|
39
|
+
* visibility openapi3 renders it at. Such a type's request reading lets that property be left out
|
|
40
|
+
* and its response reading does not, so the two are two declarations; a type reaching none is one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function requestReadsApart(program: Program, type: Type, at: Visibility): boolean;
|
|
43
|
+
/** The first required read-only property `type` reaches at `at` - see {@link requestReadsApart}. */
|
|
44
|
+
export declare function requiredReadOnlyIn(program: Program, type: Type, at: Visibility, seen: Set<Type>): ModelProperty | undefined;
|
|
30
45
|
/** Whether a property belongs in the BODY at the current position, rather than being metadata. */
|
|
31
46
|
export declare function isPayloadProperty(program: Program, property: ModelProperty): boolean;
|
|
32
47
|
export declare function withMetadataAnnotationsIgnored<T>(ignored: boolean, run: () => T): T;
|
|
@@ -285,6 +300,31 @@ export declare function optionalDiscriminator(program: Program, union: Union, va
|
|
|
285
300
|
* at more than one. None for a union with no name, whose envelopes are written out where it is.
|
|
286
301
|
*/
|
|
287
302
|
export declare function envelopeNameOf(program: Program, union: Union, variantName: string, variant: Type): string | undefined;
|
|
303
|
+
/**
|
|
304
|
+
* **Why the OpenAPI 3.0 document refuses a `null` the spec admits**, or `undefined` where it does not -
|
|
305
|
+
* asked only where openapi3 publishes 3.0. Read off openapi3's 3.0 `unionSchema` and OAS 3.0.4's
|
|
306
|
+
* `nullable`, which "only takes effect if `type` is explicitly defined within the same Schema Object",
|
|
307
|
+
* while "Other Schema Object constraints retain their defined behavior":
|
|
308
|
+
*
|
|
309
|
+
* - Each JSON kind of literal is one `{ type, enum }` member, and the `enum` refuses `null`.
|
|
310
|
+
* - **One member**, or any member a `$ref`, is written with `nullable` beside it: a `$ref` is wrapped
|
|
311
|
+
* as `{ type?, allOf: [$ref], nullable }` and the component refuses `null` unless it has no type; an
|
|
312
|
+
* inline schema admits it where it carries a `type` - a standard scalar, a list, a record, a model
|
|
313
|
+
* expression - or none at all.
|
|
314
|
+
* - **Several inline members** are `{ anyOf: [...], nullable: true }`, with no `type`, so `nullable`
|
|
315
|
+
* does nothing and only a member admitting `null` on its own does.
|
|
316
|
+
* - A bare `null` is `{ nullable: true }` - any value - and a union of nothing but `null` is refused by
|
|
317
|
+
* openapi3 itself, as `union-null`.
|
|
318
|
+
*/
|
|
319
|
+
export declare function unpublishableNullWhy(program: Program, type: Type): string | undefined;
|
|
320
|
+
/**
|
|
321
|
+
* **Everything a service declares that its 3.0 document cannot publish, reported once each**,
|
|
322
|
+
* whichever walk renders it or none: a `null` in a union, named or written where it is used, a bare
|
|
323
|
+
* `null`, and a sealed model's record - see `unpublishable-null` and `unpublishable-record`.
|
|
324
|
+
* The walks refuse each as they reach it; this is what makes sure every one is named, the way a
|
|
325
|
+
* property's `T | null` is peeled off before any union is rendered.
|
|
326
|
+
*/
|
|
327
|
+
export declare function reportUnpublishable(program: Program, service: Namespace): void;
|
|
288
328
|
/**
|
|
289
329
|
* Every property the model declares, **including the ones it inherits**.
|
|
290
330
|
*
|
|
@@ -697,6 +737,15 @@ export interface HeldDeclarations {
|
|
|
697
737
|
readonly holdsRefusalAt: (type: Type, at: Visibility) => boolean;
|
|
698
738
|
}
|
|
699
739
|
export declare function withHeldDeclarations<T>(given: HeldDeclarations, run: () => T): T;
|
|
740
|
+
/**
|
|
741
|
+
* **The record a sealed model's 3.0 document cannot see**, or `undefined`. openapi3's 3.0 sealing is
|
|
742
|
+
* `additionalProperties: { not: {} }` beside the model's own `properties` - it re-declares a base's
|
|
743
|
+
* properties there, so they survive - and a base that is a record declares none: `model M extends
|
|
744
|
+
* Record<string> { name: string }` sealed refuses every key the record admits, where 3.1's
|
|
745
|
+
* `unevaluatedProperties` sees the record through `allOf`. Mirrors {@link isSealed}: sealed here,
|
|
746
|
+
* with an ancestor whose indexer admits a value.
|
|
747
|
+
*/
|
|
748
|
+
export declare function unpublishableRecordOf(program: Program, model: Model): Model | undefined;
|
|
700
749
|
/**
|
|
701
750
|
* A model as the object it publishes at the current position: the merge of one - see
|
|
702
751
|
* {@link mergeShapes}. **HTTP metadata is not payload**, and neither is a property invisible here or
|
package/dist/src/zod.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDiscriminatedUnion, getDiscriminatedUnionFromInheritance, getDiscriminator, getDoc, getEncode, getTypeName, ignoreDiagnostics, resolveEncodedName, serializeValueAsJson, isNeverType, isTemplateDeclaration, } from "@typespec/compiler";
|
|
1
|
+
import { getDiscriminatedUnion, getDiscriminatedUnionFromInheritance, getDiscriminator, getDoc, getEncode, getTypeName, ignoreDiagnostics, isGlobalNamespace, navigateTypesInNamespace, resolveEncodedName, serializeValueAsJson, isNeverType, isTemplateDeclaration, } from "@typespec/compiler";
|
|
2
2
|
import { createMetadataInfo, getCookieParamOptions, getHeaderFieldName, getVisibilitySuffix, getPathParamName, getQueryParamName, isCookieParam, isHeader, isPathParam, isQueryParam, Visibility, } from "@typespec/http";
|
|
3
3
|
import { $ } from "@typespec/compiler/typekit";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -7,6 +7,7 @@ import { applyConstraints, firstReportOf, brokenConstraintOf, brokenItemsOf, has
|
|
|
7
7
|
import { literal } from "./literal.js";
|
|
8
8
|
import { isPublishedSplit, isSharedRouteOptional, isSuffixedByUsage, keptVisibilityOf, readVisibilityOf, } from "./published.js";
|
|
9
9
|
import { reportDiagnostic } from "./lib.js";
|
|
10
|
+
import { publishesOpenApi30, publishesOpenApi30Only } from "./openapi-versions.js";
|
|
10
11
|
/**
|
|
11
12
|
* What a walk returns after reporting a refusal.
|
|
12
13
|
*
|
|
@@ -109,7 +110,7 @@ function isSealed(model) {
|
|
|
109
110
|
/**
|
|
110
111
|
* Whether `@typespec/openapi3` is sealing object schemas for this compile.
|
|
111
112
|
*
|
|
112
|
-
* **
|
|
113
|
+
* **The consumer states it twice, here and in openapi3's own options.**
|
|
113
114
|
* Awkward, and the alternative is worse: assume sealing is on and a project running openapi3 with
|
|
114
115
|
* its default gets validators that reject properties its published document permits. Whichever way
|
|
115
116
|
* the two are configured, the conformance differential compares openness in both directions and
|
|
@@ -187,7 +188,109 @@ export function withVisibility(next, run) {
|
|
|
187
188
|
}
|
|
188
189
|
/** Whether a property is optional AT THE CURRENT POSITION - PATCH makes update fields optional. */
|
|
189
190
|
export function isOptionalAt(program, property) {
|
|
190
|
-
return (metadataInfoOf(program).isOptional(property, visibility) ||
|
|
191
|
+
return (metadataInfoOf(program).isOptional(property, visibility) ||
|
|
192
|
+
isSharedRouteOptional(property) ||
|
|
193
|
+
(requestReading !== undefined && isReadonlyProperty(program, property)));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* **A request read as an OpenAPI 3.0 document reads it: the request's visibility, while the walk is
|
|
197
|
+
* inside one, where openapi3 publishes only 3.0.**
|
|
198
|
+
*
|
|
199
|
+
* openapi3 shares a read-only property into the schema a request is published with - a create body
|
|
200
|
+
* with nothing create-specific IS the canonical schema, `id` required and `readOnly` - and OAS 3.0.4
|
|
201
|
+
* says such a `required` "will take effect on the response only". So at 3.0 a request may leave it
|
|
202
|
+
* out, and the validator lets it, as 0.30.0 did; 0.31.0 read every request as 3.1 does, where
|
|
203
|
+
* `readOnly` is an annotation and `required` holds both ways, and refused every create body leaving
|
|
204
|
+
* out a server-assigned id while openapi3 published 3.0 by default.
|
|
205
|
+
*
|
|
206
|
+
* A declaration a request reads apart from its response is declared again for the request - keyed and
|
|
207
|
+
* named for it, `WidgetCreate` beside `Widget` - see {@link requestReadsApart}.
|
|
208
|
+
*/
|
|
209
|
+
let requestReading;
|
|
210
|
+
export function withRequestReading(program, requested, run) {
|
|
211
|
+
const previous = requestReading;
|
|
212
|
+
requestReading = publishesOpenApi30Only(program) ? requested : undefined;
|
|
213
|
+
try {
|
|
214
|
+
return run();
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
requestReading = previous;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** A declaration's recorded reading, restored to render it again - see `Declaration.reading`. */
|
|
221
|
+
export function withRequestReadingAs(reading, run) {
|
|
222
|
+
const previous = requestReading;
|
|
223
|
+
requestReading = reading;
|
|
224
|
+
try {
|
|
225
|
+
return run();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
requestReading = previous;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** The request visibility a 3.0 request reading is under, or `undefined` outside one. */
|
|
232
|
+
export function requestReadingAt() {
|
|
233
|
+
return requestReading;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* **Whether a type, rendered at `at`, is read apart in a 3.0 request**: it reaches a required
|
|
237
|
+
* read-only property openapi3 publishes in it - through its own and inherited properties, a list's
|
|
238
|
+
* elements, a record's values, a union's members and a discriminated base's subtypes, each at the
|
|
239
|
+
* visibility openapi3 renders it at. Such a type's request reading lets that property be left out
|
|
240
|
+
* and its response reading does not, so the two are two declarations; a type reaching none is one.
|
|
241
|
+
*/
|
|
242
|
+
export function requestReadsApart(program, type, at) {
|
|
243
|
+
return requiredReadOnlyIn(program, type, at, new Set()) !== undefined;
|
|
244
|
+
}
|
|
245
|
+
/** The first required read-only property `type` reaches at `at` - see {@link requestReadsApart}. */
|
|
246
|
+
export function requiredReadOnlyIn(program, type, at, seen) {
|
|
247
|
+
if (seen.has(type))
|
|
248
|
+
return undefined;
|
|
249
|
+
seen.add(type);
|
|
250
|
+
const inner = (next) => requiredReadOnlyIn(program, next, renderedVisibilityOf(program, next, at), seen);
|
|
251
|
+
switch (type.kind) {
|
|
252
|
+
case "Model": {
|
|
253
|
+
if (type.indexer !== undefined) {
|
|
254
|
+
const found = inner(type.indexer.value);
|
|
255
|
+
if (found !== undefined)
|
|
256
|
+
return found;
|
|
257
|
+
}
|
|
258
|
+
const info = metadataInfoOf(program);
|
|
259
|
+
for (const { property } of flattenedPropertiesOf(program, type)) {
|
|
260
|
+
if (!info.isPayloadProperty(property, at, metadataIgnored))
|
|
261
|
+
continue;
|
|
262
|
+
if (isReadonlyProperty(program, property) && !property.optional)
|
|
263
|
+
return property;
|
|
264
|
+
const found = inner(property.type);
|
|
265
|
+
if (found !== undefined)
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
for (const derived of type.derivedModels) {
|
|
269
|
+
const found = requiredReadOnlyIn(program, derived, at, seen);
|
|
270
|
+
if (found !== undefined)
|
|
271
|
+
return found;
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
case "Union":
|
|
276
|
+
for (const variant of type.variants.values()) {
|
|
277
|
+
const found = inner(variant.type);
|
|
278
|
+
if (found !== undefined)
|
|
279
|
+
return found;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
case "UnionVariant":
|
|
283
|
+
return inner(type.type);
|
|
284
|
+
case "Tuple":
|
|
285
|
+
for (const value of type.values) {
|
|
286
|
+
const found = inner(value);
|
|
287
|
+
if (found !== undefined)
|
|
288
|
+
return found;
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
default:
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
191
294
|
}
|
|
192
295
|
/** Whether a property belongs in the BODY at the current position, rather than being metadata. */
|
|
193
296
|
export function isPayloadProperty(program, property) {
|
|
@@ -1017,7 +1120,150 @@ export function envelopeNameOf(program, union, variantName, variant) {
|
|
|
1017
1120
|
const suffixed = envelopeAt !== Visibility.Read && isSuffixedByUsage(union);
|
|
1018
1121
|
return `${union.name}${capitalise(variantName)}${suffixed ? getVisibilitySuffix(envelopeAt, Visibility.Read) : ""}`;
|
|
1019
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* **Why the OpenAPI 3.0 document refuses a `null` the spec admits**, or `undefined` where it does not -
|
|
1125
|
+
* asked only where openapi3 publishes 3.0. Read off openapi3's 3.0 `unionSchema` and OAS 3.0.4's
|
|
1126
|
+
* `nullable`, which "only takes effect if `type` is explicitly defined within the same Schema Object",
|
|
1127
|
+
* while "Other Schema Object constraints retain their defined behavior":
|
|
1128
|
+
*
|
|
1129
|
+
* - Each JSON kind of literal is one `{ type, enum }` member, and the `enum` refuses `null`.
|
|
1130
|
+
* - **One member**, or any member a `$ref`, is written with `nullable` beside it: a `$ref` is wrapped
|
|
1131
|
+
* as `{ type?, allOf: [$ref], nullable }` and the component refuses `null` unless it has no type; an
|
|
1132
|
+
* inline schema admits it where it carries a `type` - a standard scalar, a list, a record, a model
|
|
1133
|
+
* expression - or none at all.
|
|
1134
|
+
* - **Several inline members** are `{ anyOf: [...], nullable: true }`, with no `type`, so `nullable`
|
|
1135
|
+
* does nothing and only a member admitting `null` on its own does.
|
|
1136
|
+
* - A bare `null` is `{ nullable: true }` - any value - and a union of nothing but `null` is refused by
|
|
1137
|
+
* openapi3 itself, as `union-null`.
|
|
1138
|
+
*/
|
|
1139
|
+
export function unpublishableNullWhy(program, type) {
|
|
1140
|
+
if (!publishesOpenApi30(program))
|
|
1141
|
+
return undefined;
|
|
1142
|
+
if (type.kind === "Intrinsic" && type.name === "null") {
|
|
1143
|
+
return "a bare 'null' is published as '{ nullable: true }', which is any value";
|
|
1144
|
+
}
|
|
1145
|
+
if (type.kind !== "Union" || getDiscriminatedUnion(program, type)[0] !== undefined)
|
|
1146
|
+
return undefined;
|
|
1147
|
+
const variants = [...type.variants.values()].map((variant) => variant.type);
|
|
1148
|
+
if (!variants.some(isNull))
|
|
1149
|
+
return undefined;
|
|
1150
|
+
const real = variants.filter((variant) => !isNull(variant));
|
|
1151
|
+
if (real.length === 0) {
|
|
1152
|
+
return "a union of nothing but 'null' has no 3.0 schema, and openapi3 refuses it as 'union-null'";
|
|
1153
|
+
}
|
|
1154
|
+
if (admitsNullAt30(program, type, new Set()))
|
|
1155
|
+
return undefined;
|
|
1156
|
+
const referenced = real.some((member) => publishedAsReference(program, member));
|
|
1157
|
+
return referenced
|
|
1158
|
+
? "'nullable' is published beside a reference, which still refuses null - OAS 3.0.4 lets 'nullable' take effect only beside 'type', and keeps every other constraint"
|
|
1159
|
+
: "'nullable' is published beside an 'enum' or an 'anyOf' that refuses null - OAS 3.0.4 lets 'nullable' take effect only beside 'type', and keeps every other constraint";
|
|
1160
|
+
}
|
|
1161
|
+
/** Whether openapi3's 3.0 schema for `union` admits `null` - see {@link unpublishableNullWhy}. */
|
|
1162
|
+
function admitsNullAt30(program, union, seen) {
|
|
1163
|
+
if (seen.has(union))
|
|
1164
|
+
return false;
|
|
1165
|
+
seen.add(union);
|
|
1166
|
+
const variants = [...union.variants.values()].map((variant) => variant.type);
|
|
1167
|
+
const nullable = variants.some(isNull);
|
|
1168
|
+
const real = variants.filter((variant) => !isNull(variant));
|
|
1169
|
+
const literal = (member) => member.kind === "EnumMember" ||
|
|
1170
|
+
(member.kind !== "UnionVariant" && literalValueOf(member) !== undefined);
|
|
1171
|
+
const members = real.filter((member) => !literal(member));
|
|
1172
|
+
const groups = new Set(real.filter(literal).map((member) => member.kind)).size;
|
|
1173
|
+
const wrapped = nullable &&
|
|
1174
|
+
(members.length + groups === 1 ||
|
|
1175
|
+
members.some((member) => publishedAsReference(program, member)));
|
|
1176
|
+
return members.some((member) => memberAdmitsNullAt30(program, member, wrapped, seen));
|
|
1177
|
+
}
|
|
1178
|
+
/** Whether one member of a 3.0 union admits `null`, with `nullable` beside it or not. */
|
|
1179
|
+
function memberAdmitsNullAt30(program, member, wrapped, seen) {
|
|
1180
|
+
if (member.kind === "UnionVariant")
|
|
1181
|
+
return memberAdmitsNullAt30(program, member.type, wrapped, seen);
|
|
1182
|
+
if (isTypelessMember(program, member))
|
|
1183
|
+
return true;
|
|
1184
|
+
if (publishedAsReference(program, member)) {
|
|
1185
|
+
// The component answers: a named union by its own members, anything else typed and refusing.
|
|
1186
|
+
return member.kind === "Union" && admitsNullAt30(program, member, seen);
|
|
1187
|
+
}
|
|
1188
|
+
if (member.kind === "Union")
|
|
1189
|
+
return admitsNullAt30(program, member, seen);
|
|
1190
|
+
// Inline and typed - a standard scalar, a list, a record, a model expression: `nullable` beside it.
|
|
1191
|
+
return wrapped;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* **Whether openapi3 publishes `type` as a `$ref` to a component**: a named model, enum or named union
|
|
1195
|
+
* it can name, and a scalar outside the standard library. Everything else it writes out where it is
|
|
1196
|
+
* used. Mirrors `declaredNameOf`, plus scalars, which this emitter validates inline and openapi3
|
|
1197
|
+
* declares.
|
|
1198
|
+
*/
|
|
1199
|
+
function publishedAsReference(program, type) {
|
|
1200
|
+
switch (type.kind) {
|
|
1201
|
+
case "UnionVariant":
|
|
1202
|
+
return publishedAsReference(program, type.type);
|
|
1203
|
+
case "Enum":
|
|
1204
|
+
return true;
|
|
1205
|
+
case "Union":
|
|
1206
|
+
return type.name !== undefined && isSpeakableInstance(type) && !shouldInline(program, type);
|
|
1207
|
+
case "Scalar":
|
|
1208
|
+
return !program.checker.isStdType(type);
|
|
1209
|
+
case "Model":
|
|
1210
|
+
return (type.name !== "" &&
|
|
1211
|
+
!isArrayLiteral(program, type) &&
|
|
1212
|
+
!(type.name === "Record" && program.checker.isStdType(type)) &&
|
|
1213
|
+
isSpeakableInstance(type) &&
|
|
1214
|
+
!shouldInline(program, type));
|
|
1215
|
+
default:
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* **Everything a service declares that its 3.0 document cannot publish, reported once each**,
|
|
1221
|
+
* whichever walk renders it or none: a `null` in a union, named or written where it is used, a bare
|
|
1222
|
+
* `null`, and a sealed model's record - see `unpublishable-null` and `unpublishable-record`.
|
|
1223
|
+
* The walks refuse each as they reach it; this is what makes sure every one is named, the way a
|
|
1224
|
+
* property's `T | null` is peeled off before any union is rendered.
|
|
1225
|
+
*/
|
|
1226
|
+
export function reportUnpublishable(program, service) {
|
|
1227
|
+
if (!publishesOpenApi30(program))
|
|
1228
|
+
return;
|
|
1229
|
+
const check = (type) => {
|
|
1230
|
+
const why = unpublishableNullWhy(program, type);
|
|
1231
|
+
if (why !== undefined)
|
|
1232
|
+
refuseUnpublishableNull(program, type, why);
|
|
1233
|
+
};
|
|
1234
|
+
navigateTypesInNamespace(service, {
|
|
1235
|
+
union: check,
|
|
1236
|
+
// A bare `null` is one type everywhere, so it is reported where it is used.
|
|
1237
|
+
modelProperty: (property) => {
|
|
1238
|
+
if (property.type.kind !== "Intrinsic")
|
|
1239
|
+
return;
|
|
1240
|
+
const why = unpublishableNullWhy(program, property.type);
|
|
1241
|
+
if (why !== undefined)
|
|
1242
|
+
refuseUnpublishableNull(program, property.type, why, property);
|
|
1243
|
+
},
|
|
1244
|
+
model: (model) => {
|
|
1245
|
+
const record = unpublishableRecordOf(program, model);
|
|
1246
|
+
if (record !== undefined)
|
|
1247
|
+
refuseUnpublishableRecord(program, model, record);
|
|
1248
|
+
},
|
|
1249
|
+
}, { skipSubNamespaces: isGlobalNamespace(program, service) });
|
|
1250
|
+
}
|
|
1251
|
+
/** Refused where the 3.0 document cannot publish its `null` - see `unpublishable-null`. */
|
|
1252
|
+
function refuseUnpublishableNull(program, type, why, at = refusalTarget(type)) {
|
|
1253
|
+
const target = at;
|
|
1254
|
+
if (firstReportOf(program, "unpublishable-null", target)) {
|
|
1255
|
+
reportDiagnostic(program, {
|
|
1256
|
+
code: "unpublishable-null",
|
|
1257
|
+
target,
|
|
1258
|
+
format: { type: getTypeName(type), why },
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
return UNREPRESENTABLE;
|
|
1262
|
+
}
|
|
1020
1263
|
function unionToZod(program, union) {
|
|
1264
|
+
const unpublishable = unpublishableNullWhy(program, union);
|
|
1265
|
+
if (unpublishable !== undefined)
|
|
1266
|
+
return refuseUnpublishableNull(program, union, unpublishable);
|
|
1021
1267
|
/**
|
|
1022
1268
|
* A `@discriminated` union becomes `z.discriminatedUnion`, not `z.union`.
|
|
1023
1269
|
*
|
|
@@ -2990,7 +3236,40 @@ export function withHeldDeclarations(given, run) {
|
|
|
2990
3236
|
held = previous;
|
|
2991
3237
|
}
|
|
2992
3238
|
}
|
|
3239
|
+
/**
|
|
3240
|
+
* **The record a sealed model's 3.0 document cannot see**, or `undefined`. openapi3's 3.0 sealing is
|
|
3241
|
+
* `additionalProperties: { not: {} }` beside the model's own `properties` - it re-declares a base's
|
|
3242
|
+
* properties there, so they survive - and a base that is a record declares none: `model M extends
|
|
3243
|
+
* Record<string> { name: string }` sealed refuses every key the record admits, where 3.1's
|
|
3244
|
+
* `unevaluatedProperties` sees the record through `allOf`. Mirrors {@link isSealed}: sealed here,
|
|
3245
|
+
* with an ancestor whose indexer admits a value.
|
|
3246
|
+
*/
|
|
3247
|
+
export function unpublishableRecordOf(program, model) {
|
|
3248
|
+
if (!publishesOpenApi30(program) || model.indexer !== undefined || !isSealed(model)) {
|
|
3249
|
+
return undefined;
|
|
3250
|
+
}
|
|
3251
|
+
for (let base = model.baseModel; base !== undefined; base = base.baseModel) {
|
|
3252
|
+
if (base.indexer !== undefined && !isNeverType(base.indexer.value))
|
|
3253
|
+
return base;
|
|
3254
|
+
}
|
|
3255
|
+
return undefined;
|
|
3256
|
+
}
|
|
3257
|
+
/** Refused where the 3.0 document seals away a base's record - see `unpublishable-record`. */
|
|
3258
|
+
function refuseUnpublishableRecord(program, model, base) {
|
|
3259
|
+
const target = refusalTarget(model);
|
|
3260
|
+
if (firstReportOf(program, "unpublishable-record", target)) {
|
|
3261
|
+
reportDiagnostic(program, {
|
|
3262
|
+
code: "unpublishable-record",
|
|
3263
|
+
target,
|
|
3264
|
+
format: { type: getTypeName(model), base: getTypeName(base) },
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
return UNREPRESENTABLE;
|
|
3268
|
+
}
|
|
2993
3269
|
function modelToZod(program, model) {
|
|
3270
|
+
const record = unpublishableRecordOf(program, model);
|
|
3271
|
+
if (record !== undefined)
|
|
3272
|
+
return refuseUnpublishableRecord(program, model, record);
|
|
2994
3273
|
/**
|
|
2995
3274
|
* **A `@discriminator` base is its own object, deserialized as a subtype where the value is one.**
|
|
2996
3275
|
* See {@link DiscriminatedBase}.
|
|
@@ -4284,6 +4563,14 @@ function propertyValueToZod(program, property, position = OWN_POSITION) {
|
|
|
4284
4563
|
* `.optional()` stays outermost: a constraint after it would apply to the optionality rather than
|
|
4285
4564
|
* to the value.
|
|
4286
4565
|
*/
|
|
4566
|
+
// A `null` the 3.0 document cannot publish refuses the value - see `unpublishableNullWhy`.
|
|
4567
|
+
const unpublishable = unpublishableNullWhy(program, property.type);
|
|
4568
|
+
if (unpublishable !== undefined) {
|
|
4569
|
+
return {
|
|
4570
|
+
base: refuseUnpublishableNull(program, property.type, unpublishable),
|
|
4571
|
+
description: getDoc(program, property),
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4287
4574
|
/**
|
|
4288
4575
|
* `@encode` is read as `encodedValueOf` says the document publishes it, nullability included:
|
|
4289
4576
|
* openapi3 3.1 publishes `anyOf: [<the encoded value>, { type: "null" }]` where the encoding is
|
|
@@ -5144,15 +5431,18 @@ export function typeToZodBody(program, type) {
|
|
|
5144
5431
|
*/
|
|
5145
5432
|
case "UnionVariant":
|
|
5146
5433
|
return typeToZod(program, type.type);
|
|
5147
|
-
case "Intrinsic":
|
|
5148
|
-
if (type.name === "null")
|
|
5149
|
-
|
|
5434
|
+
case "Intrinsic": {
|
|
5435
|
+
if (type.name === "null") {
|
|
5436
|
+
const why = unpublishableNullWhy(program, type);
|
|
5437
|
+
return why === undefined ? "z.null()" : refuseUnpublishableNull(program, type, why);
|
|
5438
|
+
}
|
|
5150
5439
|
if (type.name === "unknown")
|
|
5151
5440
|
return "z.unknown()";
|
|
5152
5441
|
if (type.name === "void" || type.name === "never") {
|
|
5153
5442
|
return refuse(program, type, `\`${type.name}\` has no runtime representation`, "Give the position a type a value can inhabit. A `never` property or parameter is dropped to match the document, and `Record<never>` seals the model, so none of those reaches here.");
|
|
5154
5443
|
}
|
|
5155
5444
|
return refuse(program, type, `intrinsic "${type.name}"`, "This emitter has no rule for that intrinsic. Replace it with a declared scalar or model.");
|
|
5445
|
+
}
|
|
5156
5446
|
/**
|
|
5157
5447
|
* **openapi3 publishes a tuple as `prefixItems`, with no `items` or `minItems` beside it**:
|
|
5158
5448
|
* any length, each position checked where it is present, anything past the last. That is a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "TypeSpec emitter and library: turn an HTTP service definition into Zod validators and framework-free contract types that agree, keyword for keyword, with the OpenAPI document @typespec/openapi3 publishes from the same source.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|