typespec-http-zod 0.27.0 → 0.29.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 +6 -6
- package/dist/src/api.d.ts +61 -9
- package/dist/src/api.js +767 -308
- package/dist/src/constraints.d.ts +1 -1
- package/dist/src/constraints.js +59 -21
- package/dist/src/lib.d.ts +19 -1
- package/dist/src/lib.js +42 -0
- package/dist/src/literal.d.ts +27 -0
- package/dist/src/literal.js +33 -0
- package/dist/src/registry.js +54 -10
- package/dist/src/results.d.ts +113 -0
- package/dist/src/results.js +135 -0
- package/dist/src/statuses.d.ts +24 -0
- package/dist/src/statuses.js +27 -0
- package/dist/src/types.js +42 -28
- package/dist/src/uri-template.d.ts +1 -13
- package/dist/src/uri-template.js +7 -3
- package/dist/src/zod.d.ts +49 -1
- package/dist/src/zod.js +224 -24
- package/package.json +14 -14
package/dist/src/zod.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { getDiscriminatedUnion, getDiscriminator, getEncode, resolveEncodedName, isNeverType, isTemplateDeclaration, } from "@typespec/compiler";
|
|
2
|
-
import { createMetadataInfo, isQueryParam, Visibility } from "@typespec/http";
|
|
1
|
+
import { getDiscriminatedUnion, getDiscriminator, getDoc, getEncode, resolveEncodedName, isNeverType, isTemplateDeclaration, } from "@typespec/compiler";
|
|
2
|
+
import { createMetadataInfo, getCookieParamOptions, getHeaderFieldName, getPathParamName, getQueryParamName, isCookieParam, isHeader, isPathParam, isQueryParam, Visibility, } from "@typespec/http";
|
|
3
3
|
import { applyConstraints } from "./constraints.js";
|
|
4
|
+
import { literal } from "./literal.js";
|
|
4
5
|
import { reportDiagnostic } from "./lib.js";
|
|
5
6
|
/**
|
|
6
7
|
* What a walk returns after reporting a refusal.
|
|
@@ -406,10 +407,57 @@ const SCALARS = {
|
|
|
406
407
|
url: "z.url()",
|
|
407
408
|
};
|
|
408
409
|
function scalarToZod(program, scalar) {
|
|
410
|
+
return described(scalarSchemaOf(program, scalar), scalarDocOf(program, scalar));
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* **A doc becomes `.describe()`, which is the one place Zod keeps a description.**
|
|
414
|
+
*
|
|
415
|
+
* `@typespec/openapi3` writes every `getDoc` as a JSON Schema `description`, and nothing carried it
|
|
416
|
+
* into the validators: a surface publishing its input schema FROM them - an MCP server's
|
|
417
|
+
* `tools/list` - gave a model calling a tool bare patterns and enums with no word on what a field is
|
|
418
|
+
* for. Measured on one consumer: a tool refused 369 calls and accepted 98, most refusals coming from
|
|
419
|
+
* the client checking that published schema before it called.
|
|
420
|
+
*
|
|
421
|
+
* `.describe()` records the text in `z.globalRegistry`, which is a `globalThis` singleton shared by
|
|
422
|
+
* every copy of Zod in a process, and both `z.toJSONSchema` and Zod's Standard Schema converter read
|
|
423
|
+
* it by default. It returns a clone and changes nothing a parse decides.
|
|
424
|
+
*
|
|
425
|
+
* **It is always the OUTERMOST call at a position, and never on a bare reference.** Inside
|
|
426
|
+
* `.nullable()` the text lands in `anyOf[0]` rather than on the property; on an identifier inside
|
|
427
|
+
* `z.array()` or a union it would clone a declaration that other code finds by identity. So it is
|
|
428
|
+
* applied where a declaration's body, a property's value or a scalar's schema is finished, and
|
|
429
|
+
* nowhere else.
|
|
430
|
+
*
|
|
431
|
+
* Gated on `undefined` rather than on truthiness, as openapi3's `applyConstraint` is: an empty
|
|
432
|
+
* `@doc("")` is published as `""`, and so is it here.
|
|
433
|
+
*/
|
|
434
|
+
export function described(expression, doc) {
|
|
435
|
+
return doc === undefined ? expression : `${expression}.describe(${literal(doc)})`;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* The description a declared scalar publishes: its own doc, else the nearest one up its `extends`
|
|
439
|
+
* chain, and never a standard type's.
|
|
440
|
+
*
|
|
441
|
+
* Both halves are openapi3's, from `#getSchemaForScalar`: a derived scalar starts from its base's
|
|
442
|
+
* schema, description included, and overwrites what it declares itself - so `scalar B extends A`
|
|
443
|
+
* with no doc publishes A's. A standard scalar's description is deleted, because "standard types are
|
|
444
|
+
* going to be inlined in the spec and we don't want the description of the scalar to show up".
|
|
445
|
+
* Without that, `string`'s own "A sequence of textual characters." would sit on every `z.string()`.
|
|
446
|
+
*/
|
|
447
|
+
export function scalarDocOf(program, scalar) {
|
|
448
|
+
for (let current = scalar; current !== undefined && !program.checker.isStdType(current); current = current.baseScalar) {
|
|
449
|
+
const doc = getDoc(program, current);
|
|
450
|
+
if (doc !== undefined)
|
|
451
|
+
return doc;
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
/** A scalar's schema without its description, for a position that publishes its own. */
|
|
456
|
+
function scalarSchemaOf(program, scalar) {
|
|
409
457
|
// `scalar unixTimestamp32 is utcDateTime;` with `@encode(int32)` on the SCALAR, not on each use.
|
|
410
458
|
const encoded = encodedTypeOf(program, scalar);
|
|
411
459
|
if (encoded !== undefined && encoded !== scalar) {
|
|
412
|
-
return applyConstraints(program,
|
|
460
|
+
return applyConstraints(program, scalarSchemaOf(program, encoded), scalar);
|
|
413
461
|
}
|
|
414
462
|
let current = scalar;
|
|
415
463
|
// Walk the `extends` chain so a named scalar (`scalar chNumber extends string`) resolves to the
|
|
@@ -454,7 +502,7 @@ function scalarToZod(program, scalar) {
|
|
|
454
502
|
function enumToZod(_program, target) {
|
|
455
503
|
const values = [...target.members.values()].map((member) => member.value ?? member.name);
|
|
456
504
|
if (values.some((value) => typeof value !== "string")) {
|
|
457
|
-
return `z.union([${values.map((value) => `z.literal(${
|
|
505
|
+
return `z.union([${values.map((value) => `z.literal(${literal(value)})`).join(", ")}])`;
|
|
458
506
|
}
|
|
459
507
|
/**
|
|
460
508
|
* Every string vocabulary resolves through the generated constant rather than being restated.
|
|
@@ -475,7 +523,7 @@ function enumToZod(_program, target) {
|
|
|
475
523
|
*/
|
|
476
524
|
const shared = contractsPackage();
|
|
477
525
|
if (shared === undefined) {
|
|
478
|
-
return `z.enum([${values.map((value) =>
|
|
526
|
+
return `z.enum([${values.map((value) => literal(value)).join(", ")}])`;
|
|
479
527
|
}
|
|
480
528
|
noteExternalImport(shared, "SPEC_VOCABULARIES");
|
|
481
529
|
return `z.enum(SPEC_VOCABULARIES.${target.name})`;
|
|
@@ -534,7 +582,7 @@ function declaresDiscriminator(program, variant, property) {
|
|
|
534
582
|
*/
|
|
535
583
|
function discriminatedVariant(program, union, variantName, variant, options) {
|
|
536
584
|
const inner = typeToZod(program, variant);
|
|
537
|
-
const tag = `${objectKey(options.discriminatorPropertyName)}: z.literal(${
|
|
585
|
+
const tag = `${objectKey(options.discriminatorPropertyName)}: z.literal(${literal(variantName)})`;
|
|
538
586
|
// The variant already carries the discriminator, so the document publishes it and there is
|
|
539
587
|
// nothing to add.
|
|
540
588
|
if (options.envelope === "none")
|
|
@@ -587,7 +635,7 @@ function unionToZod(program, union) {
|
|
|
587
635
|
}
|
|
588
636
|
const members = [...discriminated.variants.entries()].map(([name, variant]) => discriminatedVariant(program, union, name, variant, options));
|
|
589
637
|
const property = options.discriminatorPropertyName;
|
|
590
|
-
return `z.discriminatedUnion(${
|
|
638
|
+
return `z.discriminatedUnion(${literal(property)}, [${members.join(", ")}])`;
|
|
591
639
|
}
|
|
592
640
|
const variants = [...union.variants.values()].map((variant) => variant.type);
|
|
593
641
|
const nullable = variants.some((type) => type.kind === "Intrinsic" && type.name === "null");
|
|
@@ -602,7 +650,7 @@ function unionToZod(program, union) {
|
|
|
602
650
|
}
|
|
603
651
|
const suffix = nullable ? ".nullable()" : "";
|
|
604
652
|
if (real.every((type) => type.kind === "String")) {
|
|
605
|
-
const literals = real.map((type) =>
|
|
653
|
+
const literals = real.map((type) => literal(type.value));
|
|
606
654
|
return `z.enum([${literals.join(", ")}])${suffix}`;
|
|
607
655
|
}
|
|
608
656
|
if (real.length === 1)
|
|
@@ -698,9 +746,10 @@ function modelToZod(program, model) {
|
|
|
698
746
|
*/
|
|
699
747
|
const subtypes = discriminatedSubtypes(program, model);
|
|
700
748
|
if (subtypes.length > 0) {
|
|
701
|
-
|
|
749
|
+
// `discriminatedSubtypes` is empty unless the model has a discriminator.
|
|
750
|
+
const discriminator = getDiscriminator(program, model)?.propertyName ?? "";
|
|
702
751
|
const options = subtypes.map((derived) => typeToZod(program, derived));
|
|
703
|
-
return `z.discriminatedUnion(${
|
|
752
|
+
return `z.discriminatedUnion(${literal(discriminator)}, [${options.join(", ")}])`;
|
|
704
753
|
}
|
|
705
754
|
const indexer = effectiveIndexer(model);
|
|
706
755
|
const indexerKey = indexer?.key.name;
|
|
@@ -945,7 +994,16 @@ export function wireKindOf(program, type) {
|
|
|
945
994
|
* Exported so there is one answer to this question. There were two sites and only one of them knew.
|
|
946
995
|
*/
|
|
947
996
|
export function objectKey(name) {
|
|
948
|
-
|
|
997
|
+
/**
|
|
998
|
+
* **`__proto__` is the one name a key cannot spell plainly.** In an object literal, `__proto__:`
|
|
999
|
+
* - quoted or not - sets the object's prototype rather than declaring a key, so the emitted shape
|
|
1000
|
+
* had no such property: it vanished from the published JSON Schema, and under
|
|
1001
|
+
* `seal-object-schemas` every body carrying it was refused as an unrecognised key. A computed
|
|
1002
|
+
* key declares it, in a value and in a type alike. See `unenforceable-property-name`.
|
|
1003
|
+
*/
|
|
1004
|
+
if (name === "__proto__")
|
|
1005
|
+
return `[${literal(name)}]`;
|
|
1006
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : literal(name);
|
|
949
1007
|
}
|
|
950
1008
|
/**
|
|
951
1009
|
* A doc string as a JSDoc comment, indented, or `""` when there is none.
|
|
@@ -1063,9 +1121,56 @@ const PARAMETER_STYLE_ENCODINGS = new Set([
|
|
|
1063
1121
|
"ArrayEncoding.commaDelimited",
|
|
1064
1122
|
"ArrayEncoding.newlineDelimited",
|
|
1065
1123
|
]);
|
|
1066
|
-
|
|
1124
|
+
/**
|
|
1125
|
+
* **A date-time header with no `@encode` is an RFC 7231 HTTP-date, as if it said
|
|
1126
|
+
* `@encode(DateTimeKnownEncoding.rfc7231)`.**
|
|
1127
|
+
*
|
|
1128
|
+
* That is TypeSpec's wire form for one, and `@typespec/openapi3` publishes it as
|
|
1129
|
+
* `format: http-date` from `applyEncoding`'s `isDateTimeHeader` branch, which this emitter did not
|
|
1130
|
+
* follow. The header fell through to the scalar's default, `z.iso.datetime({ offset: true })`, so a
|
|
1131
|
+
* request sending it as the document describes was refused with 400, and a handler setting it as an
|
|
1132
|
+
* HTTP-date failed its own response contract with 500 (issue #4). The explicit encoding already
|
|
1133
|
+
* emitted `z.string()`, and this is that.
|
|
1134
|
+
*
|
|
1135
|
+
* **The predicate is openapi3's: the header's own schema carries `format: date-time`.** That is
|
|
1136
|
+
* the standard `utcDateTime` or `offsetDateTime` itself. A declared scalar extending one is
|
|
1137
|
+
* published as a `$ref`, a list carries the format on its items, and `T | null` is an `anyOf`
|
|
1138
|
+
* under 3.1 and 3.2 - the versions this package grades against - so none of those is an HTTP-date
|
|
1139
|
+
* in the document, and none is here. (3.0 publishes the nullable one as `nullable: true` beside an
|
|
1140
|
+
* `http-date` format; the graded documents do not.)
|
|
1141
|
+
*/
|
|
1142
|
+
function isDateTimeHeader(program, property) {
|
|
1143
|
+
const type = property.type;
|
|
1144
|
+
return isHeader(program, property) && type.kind === "Scalar" && isStdDateTime(program, type);
|
|
1145
|
+
}
|
|
1146
|
+
function isStdDateTime(program, scalar) {
|
|
1147
|
+
const { checker } = program;
|
|
1148
|
+
return (scalar === checker.getStdType("utcDateTime") || scalar === checker.getStdType("offsetDateTime"));
|
|
1149
|
+
}
|
|
1150
|
+
/** Whether a property's value, or a scalar, is a date-time: one of the two, or built on one. */
|
|
1151
|
+
function isDateTime(program, target) {
|
|
1152
|
+
const type = target.kind === "ModelProperty" ? (nullableInnerOf(target.type) ?? target.type) : target;
|
|
1153
|
+
for (let current = type.kind === "Scalar" ? type : undefined; current !== undefined; current = current.baseScalar) {
|
|
1154
|
+
if (isStdDateTime(program, current))
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
export function encodedTypeOf(program, target) {
|
|
1067
1160
|
const encoded = getEncode(program, target);
|
|
1068
|
-
if (encoded === undefined)
|
|
1161
|
+
if (encoded === undefined) {
|
|
1162
|
+
return target.kind === "ModelProperty" && isDateTimeHeader(program, target)
|
|
1163
|
+
? program.checker.getStdType("string")
|
|
1164
|
+
: undefined;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* **`rfc3339` on a date-time is the form it already has, so it changes nothing.** openapi3's
|
|
1168
|
+
* `mergeFormatAndEncoding` keeps `format: date-time` for it, and the type it reports, `string`,
|
|
1169
|
+
* emitted `z.string()`: the explicit spelling of the default dropped the date-time check the
|
|
1170
|
+
* document still states. Only the encodings that change the wire form - `rfc7231`,
|
|
1171
|
+
* `unixTimestamp` - change the type.
|
|
1172
|
+
*/
|
|
1173
|
+
if (encoded.encoding === "rfc3339" && isDateTime(program, target))
|
|
1069
1174
|
return undefined;
|
|
1070
1175
|
if (target.kind === "ModelProperty" &&
|
|
1071
1176
|
isQueryParam(program, target) &&
|
|
@@ -1091,9 +1196,26 @@ export function propertyToZod(program, property) {
|
|
|
1091
1196
|
// wrapper is kept: `@encode` changes what the value IS, not whether it may be absent.
|
|
1092
1197
|
const encoded = encodedTypeOf(program, property);
|
|
1093
1198
|
const declared = encoded ?? nullable ?? property.type;
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1199
|
+
const bare = nullable === undefined || encoded !== undefined;
|
|
1200
|
+
/**
|
|
1201
|
+
* **One description per position, and a property's own doc wins over its scalar's.**
|
|
1202
|
+
*
|
|
1203
|
+
* openapi3 publishes `subject: Subject` as `{ $ref: Subject, description: <the property's doc> }`,
|
|
1204
|
+
* and a reader takes the sibling over what the reference carries. A scalar here is inlined rather
|
|
1205
|
+
* than referenced, so where the property's value IS the scalar, its schema is taken without its
|
|
1206
|
+
* description and the one the position publishes is decided once: the property's doc, else the
|
|
1207
|
+
* scalar's. Otherwise a documented property of a documented scalar was written
|
|
1208
|
+
* `.describe(a).describe(b)`, which says the same thing in a way no one would write.
|
|
1209
|
+
*
|
|
1210
|
+
* Under `.nullable()` the scalar keeps its own, inside the wrapper: openapi3 publishes
|
|
1211
|
+
* `anyOf: [{ $ref: Subject }, { type: "null" }]` there, so the scalar's description belongs to the
|
|
1212
|
+
* branch and only the property's belongs to the property.
|
|
1213
|
+
*/
|
|
1214
|
+
const inlinedScalar = bare && declared.kind === "Scalar";
|
|
1215
|
+
const value = inlinedScalar ? scalarSchemaOf(program, declared) : typeToZod(program, declared);
|
|
1216
|
+
const constrained = applyConstraints(program, value, property);
|
|
1217
|
+
const base = bare ? constrained : `${constrained}.nullable()`;
|
|
1218
|
+
const description = getDoc(program, property) ?? (inlinedScalar ? scalarDocOf(program, declared) : undefined);
|
|
1097
1219
|
/**
|
|
1098
1220
|
* **Optionality is a function of the position too, not only of the `?` in the spec.**
|
|
1099
1221
|
*
|
|
@@ -1104,6 +1226,7 @@ export function propertyToZod(program, property) {
|
|
|
1104
1226
|
* predicate `@typespec/openapi3` uses to decide the `required` list.
|
|
1105
1227
|
*/
|
|
1106
1228
|
const optional = property.optional || isOptionalAt(program, property);
|
|
1229
|
+
reportInheritedName(program, property, optional);
|
|
1107
1230
|
/**
|
|
1108
1231
|
* **A default only applies where the document says the property may be absent.**
|
|
1109
1232
|
*
|
|
@@ -1125,7 +1248,7 @@ export function propertyToZod(program, property) {
|
|
|
1125
1248
|
target: property,
|
|
1126
1249
|
format: { name: property.name },
|
|
1127
1250
|
});
|
|
1128
|
-
return base;
|
|
1251
|
+
return described(base, description);
|
|
1129
1252
|
}
|
|
1130
1253
|
/**
|
|
1131
1254
|
* A TypeSpec default (`basis?: string = "s1159"`) becomes `.default(...)`, which in Zod also makes
|
|
@@ -1136,7 +1259,7 @@ export function propertyToZod(program, property) {
|
|
|
1136
1259
|
*/
|
|
1137
1260
|
const defaulted = defaultOf(program, property);
|
|
1138
1261
|
if (defaulted !== undefined)
|
|
1139
|
-
return `${base}.default(${defaulted})
|
|
1262
|
+
return described(`${base}.default(${defaulted})`, description);
|
|
1140
1263
|
/**
|
|
1141
1264
|
* **`.exactOptional()`, because the document says the KEY may be absent and nothing more.**
|
|
1142
1265
|
*
|
|
@@ -1155,7 +1278,66 @@ export function propertyToZod(program, property) {
|
|
|
1155
1278
|
* differential's serialisation axis is untouched, and `def.type` is still `"optional"`, so the
|
|
1156
1279
|
* shape describers read it unchanged.
|
|
1157
1280
|
*/
|
|
1158
|
-
return optional ? `${base}.exactOptional()` : base;
|
|
1281
|
+
return described(optional ? `${base}.exactOptional()` : base, description);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* The names every JavaScript object inherits, which Zod reads in place of an absent property.
|
|
1285
|
+
* `__proto__` is among them and is the different case: Zod skips it outright.
|
|
1286
|
+
*/
|
|
1287
|
+
const INHERITED_NAMES = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
1288
|
+
/** Properties already reported, by declaration: one property renders once per visibility. */
|
|
1289
|
+
const reportedNames = new WeakMap();
|
|
1290
|
+
/**
|
|
1291
|
+
* **A wire name the validator cannot treat as an ordinary key is reported where it bites.** See
|
|
1292
|
+
* `unenforceable-property-name` and `optional-prototype-name` in `lib.ts` for why each is a
|
|
1293
|
+
* warning and what each costs.
|
|
1294
|
+
*
|
|
1295
|
+
* Reported here rather than from a walk over the program because `optional` is a fact about the
|
|
1296
|
+
* POSITION: a PATCH makes a required property optional, and only there does an inherited name
|
|
1297
|
+
* refuse its omission. Once per declaration, by its syntax node, because a visibility projection
|
|
1298
|
+
* hands this a different `ModelProperty` for the same one.
|
|
1299
|
+
*/
|
|
1300
|
+
function reportInheritedName(program, property, optional) {
|
|
1301
|
+
const name = wireNameOf(program, property);
|
|
1302
|
+
if (!INHERITED_NAMES.has(name))
|
|
1303
|
+
return;
|
|
1304
|
+
const unenforceable = name === "__proto__";
|
|
1305
|
+
if (!unenforceable && !optional)
|
|
1306
|
+
return;
|
|
1307
|
+
const declaration = property.node ?? property;
|
|
1308
|
+
let reported = reportedNames.get(program);
|
|
1309
|
+
if (reported === undefined)
|
|
1310
|
+
reportedNames.set(program, (reported = new WeakSet()));
|
|
1311
|
+
if (reported.has(declaration))
|
|
1312
|
+
return;
|
|
1313
|
+
reported.add(declaration);
|
|
1314
|
+
if (unenforceable) {
|
|
1315
|
+
reportDiagnostic(program, {
|
|
1316
|
+
code: "unenforceable-property-name",
|
|
1317
|
+
target: property,
|
|
1318
|
+
format: { name },
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
reportDiagnostic(program, {
|
|
1323
|
+
code: "optional-prototype-name",
|
|
1324
|
+
target: property,
|
|
1325
|
+
format: { name },
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/** The key a property is validated under: its parameter name, or its name in a JSON body. */
|
|
1330
|
+
function wireNameOf(program, property) {
|
|
1331
|
+
if (isHeader(program, property))
|
|
1332
|
+
return getHeaderFieldName(program, property);
|
|
1333
|
+
if (isQueryParam(program, property))
|
|
1334
|
+
return getQueryParamName(program, property) ?? property.name;
|
|
1335
|
+
if (isPathParam(program, property))
|
|
1336
|
+
return getPathParamName(program, property) ?? property.name;
|
|
1337
|
+
if (isCookieParam(program, property)) {
|
|
1338
|
+
return getCookieParamOptions(program, property)?.name ?? property.name;
|
|
1339
|
+
}
|
|
1340
|
+
return rawPropertyKey(program, property);
|
|
1159
1341
|
}
|
|
1160
1342
|
/**
|
|
1161
1343
|
* A default value as a JS literal, or `undefined` where this emitter cannot render one.
|
|
@@ -1180,7 +1362,7 @@ export function propertyToZod(program, property) {
|
|
|
1180
1362
|
*/
|
|
1181
1363
|
function renderValue(value) {
|
|
1182
1364
|
if (value.valueKind === "StringValue")
|
|
1183
|
-
return
|
|
1365
|
+
return literal(value.value);
|
|
1184
1366
|
if (value.valueKind === "NumericValue")
|
|
1185
1367
|
return String(value.value.asNumber());
|
|
1186
1368
|
if (value.valueKind === "BooleanValue")
|
|
@@ -1189,7 +1371,7 @@ function renderValue(value) {
|
|
|
1189
1371
|
return "null";
|
|
1190
1372
|
if (value.valueKind === "EnumValue") {
|
|
1191
1373
|
const member = value.value;
|
|
1192
|
-
return
|
|
1374
|
+
return literal(member.value ?? member.name);
|
|
1193
1375
|
}
|
|
1194
1376
|
if (value.valueKind === "ArrayValue") {
|
|
1195
1377
|
const rendered = value.values.map(renderValue);
|
|
@@ -1203,12 +1385,30 @@ function renderValue(value) {
|
|
|
1203
1385
|
const rendered = renderValue(descriptor.value);
|
|
1204
1386
|
if (rendered === undefined)
|
|
1205
1387
|
return undefined;
|
|
1206
|
-
entries.push(`${
|
|
1388
|
+
entries.push(`${literal(name)}: ${rendered}`);
|
|
1207
1389
|
}
|
|
1208
1390
|
return `{${entries.join(", ")}}`;
|
|
1209
1391
|
}
|
|
1210
1392
|
return undefined;
|
|
1211
1393
|
}
|
|
1394
|
+
/**
|
|
1395
|
+
* **Whether `propertyToZod` emits `.default(...)` for this property, which is exactly whether its
|
|
1396
|
+
* INPUT admits an explicit `undefined`.**
|
|
1397
|
+
*
|
|
1398
|
+
* `.exactOptional()` refuses `{ p: undefined }`; `.default(v)` accepts it and fills `v`. The contract
|
|
1399
|
+
* types in `requests.gen.ts` state the input, so they have to know which of the two this emitted -
|
|
1400
|
+
* and asking this function rather than restating its rule is what keeps the two walks agreeing.
|
|
1401
|
+
* A default on a REQUIRED property emits neither (`default-on-required-property`), and one whose
|
|
1402
|
+
* value cannot be rendered emits no `.default()` (`unsupported-default`); both fall out here.
|
|
1403
|
+
*
|
|
1404
|
+
* Pure: it reports nothing, so the type walk can ask it without duplicating a diagnostic.
|
|
1405
|
+
*/
|
|
1406
|
+
export function emitsDefault(program, property) {
|
|
1407
|
+
const optional = property.optional || isOptionalAt(program, property);
|
|
1408
|
+
return (optional &&
|
|
1409
|
+
property.defaultValue !== undefined &&
|
|
1410
|
+
renderValue(property.defaultValue) !== undefined);
|
|
1411
|
+
}
|
|
1212
1412
|
/** A property's declared default, as a JS literal, or `undefined` when it has none. */
|
|
1213
1413
|
function defaultOf(program, property) {
|
|
1214
1414
|
const value = property.defaultValue;
|
|
@@ -1312,13 +1512,13 @@ export function typeToZodBody(program, type) {
|
|
|
1312
1512
|
case "Union":
|
|
1313
1513
|
return unionToZod(program, type);
|
|
1314
1514
|
case "String":
|
|
1315
|
-
return `z.literal(${
|
|
1515
|
+
return `z.literal(${literal(type.value)})`;
|
|
1316
1516
|
case "Number":
|
|
1317
1517
|
return `z.literal(${type.value})`;
|
|
1318
1518
|
case "Boolean":
|
|
1319
1519
|
return `z.literal(${String(type.value)})`;
|
|
1320
1520
|
case "EnumMember":
|
|
1321
|
-
return `z.literal(${
|
|
1521
|
+
return `z.literal(${literal(type.value ?? type.name)})`;
|
|
1322
1522
|
/**
|
|
1323
1523
|
* **A named union VARIANT is a type you can refer to, and it is not the union.**
|
|
1324
1524
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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",
|
|
@@ -47,17 +47,17 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^26.5.1",
|
|
50
|
-
"@typespec/compiler": "1.
|
|
51
|
-
"@typespec/events": "0.
|
|
52
|
-
"@typespec/http": "1.
|
|
53
|
-
"@typespec/http-specs": "0.1.0-alpha.
|
|
54
|
-
"@typespec/openapi": "1.
|
|
55
|
-
"@typespec/openapi3": "1.
|
|
56
|
-
"@typespec/rest": "0.
|
|
57
|
-
"@typespec/sse": "0.
|
|
58
|
-
"@typespec/streams": "0.
|
|
59
|
-
"@typespec/versioning": "0.
|
|
60
|
-
"@typespec/xml": "0.
|
|
50
|
+
"@typespec/compiler": "1.17.0",
|
|
51
|
+
"@typespec/events": "0.87.0",
|
|
52
|
+
"@typespec/http": "1.17.0",
|
|
53
|
+
"@typespec/http-specs": "0.1.0-alpha.44",
|
|
54
|
+
"@typespec/openapi": "1.17.0",
|
|
55
|
+
"@typespec/openapi3": "1.17.0",
|
|
56
|
+
"@typespec/rest": "0.87.0",
|
|
57
|
+
"@typespec/sse": "0.87.0",
|
|
58
|
+
"@typespec/streams": "0.87.0",
|
|
59
|
+
"@typespec/versioning": "0.87.0",
|
|
60
|
+
"@typespec/xml": "0.87.0",
|
|
61
61
|
"ajv": "^8.20.0",
|
|
62
62
|
"oxfmt": "^0.67.0",
|
|
63
63
|
"oxlint": "^1.82.0",
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"@typespec/compiler": "^1.15.0",
|
|
72
72
|
"@typespec/http": "^1.15.0",
|
|
73
73
|
"@typespec/openapi": "^1.15.0",
|
|
74
|
-
"@typespec/streams": "^0.85.0 || ^0.86.0",
|
|
75
|
-
"@typespec/versioning": "^0.85.0 || ^0.86.0",
|
|
74
|
+
"@typespec/streams": "^0.85.0 || ^0.86.0 || ^0.87.0",
|
|
75
|
+
"@typespec/versioning": "^0.85.0 || ^0.86.0 || ^0.87.0",
|
|
76
76
|
"zod": "^4.5.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|