typespec-http-zod 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/src/api.d.ts +31 -0
- package/dist/src/api.js +1443 -360
- package/dist/src/constraints.d.ts +31 -2
- package/dist/src/constraints.js +242 -46
- package/dist/src/lib.d.ts +136 -13
- package/dist/src/lib.js +167 -18
- package/dist/src/published.d.ts +139 -0
- package/dist/src/published.js +798 -0
- package/dist/src/registry.d.ts +77 -45
- package/dist/src/registry.js +759 -198
- package/dist/src/types.d.ts +12 -2
- package/dist/src/types.js +267 -92
- package/dist/src/uri-template.d.ts +110 -8
- package/dist/src/uri-template.js +251 -46
- package/dist/src/zod.d.ts +715 -39
- package/dist/src/zod.js +4029 -547
- package/package.json +1 -1
package/dist/src/types.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { type ModelProperty, type Program, type Type } from "@typespec/compiler";
|
|
2
|
+
import { type Merged, type PropertyPosition } from "./zod.js";
|
|
3
|
+
/**
|
|
4
|
+
* **A type the document publishes that no TypeSpec type stands for** - an envelope, `PetCat` -
|
|
5
|
+
* declared under its name by the registry, as the schema registry declares its validator.
|
|
6
|
+
*/
|
|
7
|
+
export declare function withTsSyntheticDeclarations<T>(hook: (name: string, build: () => string, site: Type) => string, run: () => T): T;
|
|
8
|
+
/**
|
|
9
|
+
* A merged value as TypeScript - `z.input` of what `mergedToZod` writes for it, node for node.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mergedToTs(program: Program, value: Merged): string;
|
|
2
12
|
/**
|
|
3
13
|
* A property line, including its optionality marker.
|
|
4
14
|
*
|
|
@@ -15,8 +25,8 @@ import { type ModelProperty, type Program, type Type } from "@typespec/compiler"
|
|
|
15
25
|
* Shared with the annotation the registry writes on a deferred (getter) property, so the type that
|
|
16
26
|
* annotation claims is by construction the type this walk renders for the same property.
|
|
17
27
|
*/
|
|
18
|
-
export declare function propertyTypeToTs(program: Program, property: ModelProperty): string;
|
|
19
|
-
export declare function propertyToTs(program: Program, property: ModelProperty, wireName?: string): string;
|
|
28
|
+
export declare function propertyTypeToTs(program: Program, property: ModelProperty, position?: PropertyPosition): string;
|
|
29
|
+
export declare function propertyToTs(program: Program, property: ModelProperty, wireName?: string, position?: PropertyPosition): string;
|
|
20
30
|
export declare function withTsRefResolver<T>(resolver: (type: Type) => string | undefined, run: () => T): T;
|
|
21
31
|
export declare function typeToTs(program: Program, type: Type): string;
|
|
22
32
|
/**
|
package/dist/src/types.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { getDiscriminatedUnion,
|
|
1
|
+
import { getDiscriminatedUnion, resolveEncodedName, } from "@typespec/compiler";
|
|
2
|
+
import { firstReportOf } from "./constraints.js";
|
|
2
3
|
import { reportDiagnostic } from "./lib.js";
|
|
3
4
|
import { literal } from "./literal.js";
|
|
4
|
-
import { discriminatedBaseOf, discriminatedSubtypes, effectiveIndexer, emitsDefault,
|
|
5
|
+
import { ANY_JSON_TS, atDeclaration, atItem, atPosition, atReading, constrainedMembersOf, constrainingKeywords, discriminatedBaseOf, discriminatedSubtypes, dispatchedVariantsOf, effectiveIndexer, emitsDefault, encodedIntersects, encodedValueOf, envelopeNameOf, envelopeVisibilityOf, exclusiveSiteOf, holdsRefusedMember, implicitlyOptional, inParameterPosition, isExclusiveHere, isOptionalAt, isRawBytes, isTypelessMember, multipartBinaryContext, keptMembersOf, LIST_TEMPLATE_REMEDY, listTemplateInstanceWhy, membersToWrite, nullableInnerOf, objectKey, objectOfModel, OWN_POSITION, refusalTarget, replacesFormOf, scalarEncodingOf, sealedBaseExtensionOf, TUPLE_PARAMETER, undeclaredVariantOf, unsatisfiableSubtype, withDeclarationSite, withExclusiveSite, withVisibility, } from "./zod.js";
|
|
5
6
|
/**
|
|
6
7
|
* TypeSpec type -> plain TypeScript type text.
|
|
7
8
|
*
|
|
@@ -65,26 +66,29 @@ const UNREPRESENTABLE = "never";
|
|
|
65
66
|
* `withDeclarationSite` in `zod.ts`, whose scope this walk shares.
|
|
66
67
|
*/
|
|
67
68
|
function refuse(program, type, why, remedy = "") {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
// Once per declaration, whichever walk reaches it first: both walk every type.
|
|
70
|
+
const target = refusalTarget(type);
|
|
71
|
+
if (firstReportOf(program, "unsupported-type", target, `${type.kind} ${why}`)) {
|
|
72
|
+
reportDiagnostic(program, {
|
|
73
|
+
code: "unsupported-type",
|
|
74
|
+
target,
|
|
75
|
+
format: { artefact: "a TypeScript type", kind: type.kind, why, remedy },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
73
78
|
return UNREPRESENTABLE;
|
|
74
79
|
}
|
|
75
80
|
function scalarToTs(program, scalar) {
|
|
76
|
-
// A scalar declaration can carry the encoding itself - see `
|
|
77
|
-
const encoded =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
81
|
+
// A scalar declaration can carry the encoding itself, or inherit it - see `scalarEncodingOf`.
|
|
82
|
+
const encoded = scalarEncodingOf(program, scalar)?.encoded;
|
|
83
|
+
// A scalar that is not standard states no type; the schema walk reports it.
|
|
84
|
+
if (encoded !== undefined && !program.checker.isStdType(encoded))
|
|
85
|
+
return UNREPRESENTABLE;
|
|
86
|
+
if (encoded !== undefined)
|
|
87
|
+
return scalarToTs(program, encoded);
|
|
84
88
|
let current = scalar;
|
|
85
89
|
// Walk `extends` so `scalar TrimmedString extends string` resolves to the primitive underneath.
|
|
86
90
|
while (current !== undefined) {
|
|
87
|
-
const mapped = SCALARS[current.name];
|
|
91
|
+
const mapped = program.checker.isStdType(current) ? SCALARS[current.name] : undefined;
|
|
88
92
|
if (mapped !== undefined)
|
|
89
93
|
return mapped;
|
|
90
94
|
current = current.baseScalar;
|
|
@@ -105,6 +109,22 @@ function scalarToTs(program, scalar) {
|
|
|
105
109
|
*/
|
|
106
110
|
return "unknown";
|
|
107
111
|
}
|
|
112
|
+
/** Declares a shape no type stands for under a name - see `withTsSyntheticDeclarations`. */
|
|
113
|
+
let declareSynthetic;
|
|
114
|
+
/**
|
|
115
|
+
* **A type the document publishes that no TypeSpec type stands for** - an envelope, `PetCat` -
|
|
116
|
+
* declared under its name by the registry, as the schema registry declares its validator.
|
|
117
|
+
*/
|
|
118
|
+
export function withTsSyntheticDeclarations(hook, run) {
|
|
119
|
+
const previous = declareSynthetic;
|
|
120
|
+
declareSynthetic = hook;
|
|
121
|
+
try {
|
|
122
|
+
return run();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
declareSynthetic = previous;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
108
128
|
function enumToTs(target) {
|
|
109
129
|
const values = [...target.members.values()].map((member) => member.value ?? member.name);
|
|
110
130
|
return values.map((value) => literal(value)).join(" | ");
|
|
@@ -119,17 +139,39 @@ function unionToTs(program, union) {
|
|
|
119
139
|
const [discriminated] = getDiscriminatedUnion(program, union);
|
|
120
140
|
if (discriminated !== undefined && discriminated.defaultVariant === undefined) {
|
|
121
141
|
const { envelope, discriminatorPropertyName, envelopePropertyName } = discriminated.options;
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
// Refused by the Zod walk as `undeclared-discriminator`, and `never` on both sides.
|
|
143
|
+
if (envelope === "none" &&
|
|
144
|
+
[...discriminated.variants.values()].some((variant) => {
|
|
145
|
+
const why = undeclaredVariantOf(program, variant, discriminatorPropertyName);
|
|
146
|
+
return why === "missing" || why === "open";
|
|
147
|
+
})) {
|
|
148
|
+
return UNREPRESENTABLE;
|
|
149
|
+
}
|
|
150
|
+
// A variant no value can be is dropped, as `unionToZod` drops it.
|
|
151
|
+
const variants = dispatchedVariantsOf(program, union);
|
|
152
|
+
if (variants.length === 0)
|
|
153
|
+
return UNREPRESENTABLE;
|
|
154
|
+
const arms = variants.map(([name, variant]) => {
|
|
155
|
+
// Optional where the envelope is published at a PATCH - see `envelopeVisibilityOf`.
|
|
156
|
+
const optional = implicitlyOptional(envelopeVisibilityOf(program, union, variant)) ? "?" : "";
|
|
157
|
+
const tag = `${objectKey(discriminatorPropertyName)}${optional}: ${literal(name)};`;
|
|
124
158
|
const inner = typeToTs(program, variant);
|
|
125
159
|
// No injection: with no envelope the variant declares the discriminator itself, or the union
|
|
126
160
|
// is refused in `zod.ts`. Nothing is added that the document does not publish.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
161
|
+
if (envelope === "none")
|
|
162
|
+
return inner;
|
|
163
|
+
const wrapper = `{ ${tag} ${objectKey(envelopePropertyName)}${optional}: ${inner}; }`;
|
|
164
|
+
// Declared under the component openapi3 publishes it as, as the validator is.
|
|
165
|
+
const declared = envelopeNameOf(program, union, name, variant);
|
|
166
|
+
return declared === undefined
|
|
167
|
+
? wrapper
|
|
168
|
+
: (declareSynthetic?.(declared, () => wrapper, union) ?? wrapper);
|
|
130
169
|
});
|
|
131
170
|
return arms.join(" | ");
|
|
132
171
|
}
|
|
172
|
+
// Refused with the validator, which says why - see `holdsRefusedMember`.
|
|
173
|
+
if (holdsRefusedMember(program, union, isExclusiveHere(program, union)))
|
|
174
|
+
return UNREPRESENTABLE;
|
|
133
175
|
const variants = [...union.variants.values()].map((variant) => variant.type);
|
|
134
176
|
if (variants.length === 0) {
|
|
135
177
|
reportDiagnostic(program, {
|
|
@@ -139,7 +181,10 @@ function unionToTs(program, union) {
|
|
|
139
181
|
});
|
|
140
182
|
return UNREPRESENTABLE;
|
|
141
183
|
}
|
|
142
|
-
|
|
184
|
+
// A raw `bytes` member of a multipart part's union is any value - see `withMultipartBinary`.
|
|
185
|
+
return variants
|
|
186
|
+
.map((type) => multipartBinaryContext() && isRawBytes(program, type) ? "unknown" : typeToTs(program, type))
|
|
187
|
+
.join(" | ");
|
|
143
188
|
}
|
|
144
189
|
function modelToTs(program, model) {
|
|
145
190
|
/**
|
|
@@ -158,85 +203,79 @@ function modelToTs(program, model) {
|
|
|
158
203
|
* of that class it has found. The first was a vocabulary inlined by one walk and referenced by the
|
|
159
204
|
* other.
|
|
160
205
|
*
|
|
161
|
-
* **
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* change what it accepts as input, so `z.input` and this type stay identical.
|
|
206
|
+
* **The base itself is the last arm**, as `modelToZod` writes it: a value that is no valid instance
|
|
207
|
+
* of the subtype its discriminator names is the base's own object, which is what the document
|
|
208
|
+
* validates it as. `z.input` of the union and this type are therefore identical.
|
|
165
209
|
*/
|
|
166
210
|
const base = discriminatedBaseOf(program, model);
|
|
167
211
|
if (base !== undefined) {
|
|
168
212
|
const arms = discriminatedSubtypes(program, model).map((derived) => typeToTs(program, derived));
|
|
169
|
-
|
|
170
|
-
arms.push(modelObjectToTs(program, model, base));
|
|
213
|
+
arms.push(modelObjectToTs(program, model));
|
|
171
214
|
return arms.join(" | ");
|
|
172
215
|
}
|
|
173
216
|
return modelObjectToTs(program, model);
|
|
174
217
|
}
|
|
175
218
|
/**
|
|
176
219
|
* A model as the object of its own properties, inherited ones included - for a discriminated base,
|
|
177
|
-
*
|
|
220
|
+
* the base, with the `string` discriminator openapi3 adds where the base declares none.
|
|
178
221
|
*/
|
|
179
|
-
function modelObjectToTs(program, model
|
|
222
|
+
function modelObjectToTs(program, model) {
|
|
223
|
+
// Refused by the Zod walk - `sealed-base-extended`, `invalid-subtype-discriminator` - and `never`
|
|
224
|
+
// on both sides.
|
|
225
|
+
const extension = sealedBaseExtensionOf(program, model);
|
|
226
|
+
if (extension?.required === true || unsatisfiableSubtype(program, model))
|
|
227
|
+
return UNREPRESENTABLE;
|
|
180
228
|
const indexer = effectiveIndexer(model);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// same model, so a base whose properties reach one and not the other is a contradiction we ship.
|
|
184
|
-
const declared = inheritedAndOwnProperties(model);
|
|
185
|
-
if (indexerKey === "integer" && indexer !== undefined) {
|
|
186
|
-
const element = typeToTs(program, indexer.value);
|
|
229
|
+
if (indexer?.key.name === "integer") {
|
|
230
|
+
const element = atItem(program, model, () => typeToTs(program, indexer.value));
|
|
187
231
|
// Parenthesised so `A | B` as an element does not become `A | B[]`.
|
|
188
232
|
return /[|&\s]/.test(element) ? `(${element})[]` : `${element}[]`;
|
|
189
233
|
}
|
|
234
|
+
// Inherited properties included, each judged where it is published, and the discriminator openapi3
|
|
235
|
+
// adds - the object `modelObjectToZod` writes, from the one `objectOfModel` both walks read.
|
|
236
|
+
return mergedToTs(program, objectOfModel(program, model));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* An object as TypeScript: the same members `objectToZod` writes, a dictionary as a `Record`.
|
|
240
|
+
*
|
|
241
|
+
* **A string indexer only means "dictionary" when the model declares NO properties.** The same trap
|
|
242
|
+
* `modelToZod` had, and it bites harder here: every permissive response model is
|
|
243
|
+
* `{ ...pinned fields; ...Record<unknown> }`, so reading the indexer first would type all 97 of them
|
|
244
|
+
* as a bare `Record<string, unknown>` - a type that satisfies any assertion about what the response
|
|
245
|
+
* contains, which is precisely the check this file exists to feed.
|
|
246
|
+
*
|
|
247
|
+
* **Metadata, invisible and `never` properties are left out here, as on the Zod side, and the two
|
|
248
|
+
* must agree** - both read one member list. Two walks over one TypeSpec have to agree about which
|
|
249
|
+
* properties exist, or `wire-contract.gen.ts` pairs a validator against a type describing a
|
|
250
|
+
* different shape.
|
|
251
|
+
*/
|
|
252
|
+
/** Whether a merged dictionary is written as an object of its own - see `mergedToTs`. */
|
|
253
|
+
let spellingDictionaries = false;
|
|
254
|
+
function objectToTs(program, object) {
|
|
255
|
+
const { rest } = object;
|
|
190
256
|
/**
|
|
191
|
-
* **A
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* `{
|
|
195
|
-
* as a bare `Record<string, unknown>` - a type that satisfies any assertion about what the response
|
|
196
|
-
* contains, which is precisely the check this file exists to feed.
|
|
197
|
-
*/
|
|
198
|
-
// `Record<never>` states closedness rather than a value type - see `modelToZod`. A model that is
|
|
199
|
-
// only that is an object with no declared properties, not a dictionary of an unwalkable type.
|
|
200
|
-
if (indexerKey === "string" &&
|
|
201
|
-
indexer !== undefined &&
|
|
202
|
-
!isNeverType(indexer.value) &&
|
|
203
|
-
declared.length === 0) {
|
|
204
|
-
return `Record<string, ${typeToTs(program, indexer.value)}>`;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* **A `never` property is dropped here too, and this walk is where the refusal actually came
|
|
208
|
-
* from.** `modelToZod` filters it because `@typespec/openapi3` omits such a property from the
|
|
209
|
-
* document entirely - `model N { value: never; other: string }` publishes as `{other}` with
|
|
210
|
-
* `required: ["other"]`. This walk had no property filter at all, so it reached `typeToTs`, hit the
|
|
211
|
-
* `Intrinsic` case and reported `unsupported-type` from a type with no source node - which is why
|
|
212
|
-
* the diagnostic pointed at `<unknown location>` and why filtering the Zod side alone did not
|
|
213
|
-
* silence it.
|
|
214
|
-
*
|
|
215
|
-
* Two walks over one TypeSpec have to agree about which properties exist, or `wire-contract.gen.ts`
|
|
216
|
-
* pairs a validator against a type describing a different shape. That assertion is the only thing
|
|
217
|
-
* that catches this emitter disagreeing with itself, and it cannot catch a disagreement that
|
|
218
|
-
* aborts the compile first.
|
|
219
|
-
*/
|
|
220
|
-
/**
|
|
221
|
-
* **Metadata is filtered out here, as it is on the Zod side, and the two must agree.**
|
|
222
|
-
*
|
|
223
|
-
* `metadataInfo.isPayloadProperty` is what decides whether a property is in the BODY at this
|
|
224
|
-
* position: a `@query` or `@header` property is carried separately, and an `@invisible` one is
|
|
225
|
-
* carried nowhere. `modelToZod` has asked since it was written; this walk never did, so a
|
|
226
|
-
* contract type listed properties the validator beside it refuses as unknown keys.
|
|
227
|
-
*
|
|
228
|
-
* **Applied AFTER the dictionary check above, deliberately.** That check reads the UNFILTERED
|
|
229
|
-
* list, because a model whose only properties are metadata is still a declared object rather than
|
|
230
|
-
* a dictionary, and filtering first would flip it to `Record<string, T>` on one side alone.
|
|
257
|
+
* **A dictionary is an object with an indexer and no member where it is used** - the wire
|
|
258
|
+
* assertion's own reading of `z.input`. A model whose only properties are a `@header` or a
|
|
259
|
+
* create-only key, returned by a GET, is validated as `z.object({}).catchall(...)` there, and was
|
|
260
|
+
* typed `{}` for declaring properties, which the assertion refused.
|
|
231
261
|
*/
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
const dictionary = object.dictionary || object.members.length === 0;
|
|
263
|
+
// The library's `Record`, whatever the spec names its own - see `LIBRARY_NAMES_WRITTEN_BARE` -
|
|
264
|
+
// or an object of its own, where a union writes it twice - see `mergedToTs`.
|
|
265
|
+
const dictionaryOf = (value) => spellingDictionaries ? `{ [key: string]: ${value} }` : `globalThis.Record<string, ${value}>`;
|
|
266
|
+
if (dictionary && rest === "loose")
|
|
267
|
+
return dictionaryOf("unknown");
|
|
268
|
+
if (dictionary && typeof rest !== "string") {
|
|
269
|
+
const value = mergedToTs(program, rest);
|
|
270
|
+
/**
|
|
271
|
+
* A dictionary of nothing admits only `{}`, the empty object an empty model is written as;
|
|
272
|
+
* `Record<string, never>` is how Zod infers both, and the wire contract reads it as `{}`.
|
|
273
|
+
*/
|
|
274
|
+
return value === UNREPRESENTABLE ? "{}" : dictionaryOf(value);
|
|
239
275
|
}
|
|
276
|
+
const entries = object.members.map(({ key, property, position }) => property === undefined
|
|
277
|
+
? `\t${objectKey(key)}${position.required ? "" : "?"}: ${mergedToTs(program, position.value ?? { kind: "never" })};`
|
|
278
|
+
: `\t${withDeclarationSite(property, () => atPosition(position, () => atDeclaration(program, property, () => propertyToTs(program, property, undefined, position))))}`);
|
|
240
279
|
/**
|
|
241
280
|
* **The indexer is deliberately NOT intersected in.**
|
|
242
281
|
*
|
|
@@ -269,6 +308,73 @@ function modelObjectToTs(program, model, base) {
|
|
|
269
308
|
*/
|
|
270
309
|
return entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
|
|
271
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* A merged value as TypeScript - `z.input` of what `mergedToZod` writes for it, node for node.
|
|
313
|
+
*/
|
|
314
|
+
export function mergedToTs(program, value) {
|
|
315
|
+
switch (value.kind) {
|
|
316
|
+
case "never":
|
|
317
|
+
case "refused":
|
|
318
|
+
return UNREPRESENTABLE;
|
|
319
|
+
case "type": {
|
|
320
|
+
const typed = () => typeWithKeywordsToTs(program, value.type, value.keywords);
|
|
321
|
+
return value.at === undefined ? typed() : withVisibility(value.at, typed);
|
|
322
|
+
}
|
|
323
|
+
case "whole":
|
|
324
|
+
return propertyValueToTs(program, value.property);
|
|
325
|
+
case "nullable":
|
|
326
|
+
return `${mergedToTs(program, value.inner)} | null`;
|
|
327
|
+
case "union": {
|
|
328
|
+
// The members the validator writes - see `membersToWrite`.
|
|
329
|
+
const members = membersToWrite(program, value);
|
|
330
|
+
const texts = members.map((member) => mergedToTs(program, member));
|
|
331
|
+
/**
|
|
332
|
+
* **A dictionary written twice is written as two objects.** TypeScript reads
|
|
333
|
+
* `Record<string, unknown>` twice as the one type it names, where the validator writes two
|
|
334
|
+
* objects - `z.xor([z.looseObject({}), z.looseObject({})])`, a `@oneOf` of two merged
|
|
335
|
+
* members alike - whose two `z.input` types are two, and the wire assertion told them apart
|
|
336
|
+
* (`TS2344`). Spelled as an object, each is a type of its own, as each validator's is.
|
|
337
|
+
*/
|
|
338
|
+
return members
|
|
339
|
+
.map((member, index) => {
|
|
340
|
+
const text = texts[index];
|
|
341
|
+
if (texts.indexOf(text) === texts.lastIndexOf(text))
|
|
342
|
+
return text;
|
|
343
|
+
const previous = spellingDictionaries;
|
|
344
|
+
spellingDictionaries = true;
|
|
345
|
+
try {
|
|
346
|
+
return mergedToTs(program, member);
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
spellingDictionaries = previous;
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
.join(" | ");
|
|
353
|
+
}
|
|
354
|
+
case "all":
|
|
355
|
+
return value.parts.map((part) => `(${mergedToTs(program, part)})`).join(" & ");
|
|
356
|
+
case "array": {
|
|
357
|
+
const list = value.list;
|
|
358
|
+
const element = list === undefined
|
|
359
|
+
? mergedToTs(program, value.element)
|
|
360
|
+
: atItem(program, list, () => mergedToTs(program, value.element));
|
|
361
|
+
return /[|&\s]/.test(element) ? `(${element})[]` : `${element}[]`;
|
|
362
|
+
}
|
|
363
|
+
case "object":
|
|
364
|
+
return objectToTs(program, value);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** A type with keywords on it: the members they leave - see `keptMembersOf`. */
|
|
368
|
+
function typeWithKeywordsToTs(program, type, keywords) {
|
|
369
|
+
const sources = constrainingKeywords(program, keywords);
|
|
370
|
+
if (sources.length === 0)
|
|
371
|
+
return typeToTs(program, type);
|
|
372
|
+
const member = (each) => isTypelessMember(program, each) ? `(${ANY_JSON_TS})` : typeToTs(program, each);
|
|
373
|
+
const members = keptMembersOf(program, type, sources);
|
|
374
|
+
if (members === undefined)
|
|
375
|
+
return member(type);
|
|
376
|
+
return members.length === 0 ? UNREPRESENTABLE : members.map(member).join(" | ");
|
|
377
|
+
}
|
|
272
378
|
/**
|
|
273
379
|
* A property line, including its optionality marker.
|
|
274
380
|
*
|
|
@@ -285,7 +391,17 @@ function modelObjectToTs(program, model, base) {
|
|
|
285
391
|
* Shared with the annotation the registry writes on a deferred (getter) property, so the type that
|
|
286
392
|
* annotation claims is by construction the type this walk renders for the same property.
|
|
287
393
|
*/
|
|
288
|
-
export function propertyTypeToTs(program, property) {
|
|
394
|
+
export function propertyTypeToTs(program, property, position = OWN_POSITION) {
|
|
395
|
+
// What the declarations above it leave of the value - see `PropertyPosition`.
|
|
396
|
+
if (position.never || position.refused !== undefined)
|
|
397
|
+
return UNREPRESENTABLE;
|
|
398
|
+
if (position.nullOnly)
|
|
399
|
+
return "null";
|
|
400
|
+
if (position.value !== undefined)
|
|
401
|
+
return mergedToTs(program, position.value);
|
|
402
|
+
return atReading(program, property, () => propertyValueToTs(program, property, position));
|
|
403
|
+
}
|
|
404
|
+
function propertyValueToTs(program, property, position = OWN_POSITION) {
|
|
289
405
|
/**
|
|
290
406
|
* **`@encode` changes the wire TYPE, and `encodedTypeOf` is the one rule for when it does.** This
|
|
291
407
|
* read `getEncode(...).type` directly, so it missed the exception the Zod walk makes: an array
|
|
@@ -293,14 +409,57 @@ export function propertyTypeToTs(program, property) {
|
|
|
293
409
|
* back into the declared list. So `WireInputs` said `colors: string` for a pipe-delimited query
|
|
294
410
|
* whose handler is given `string[]` - two walks of one property, one of them knowing a rule.
|
|
295
411
|
*/
|
|
296
|
-
|
|
297
|
-
|
|
412
|
+
/**
|
|
413
|
+
* **And `encodedValueOf` is the rule for what the value then is**, read exactly as the Zod walk
|
|
414
|
+
* reads it, so `z.input` and this type agree: the declared type where the document keeps it beside
|
|
415
|
+
* the encoded scalar, the scalar where it does not, both where each constrains the other, and
|
|
416
|
+
* `never` where no value is both. A nullable property stays nullable.
|
|
417
|
+
*/
|
|
418
|
+
const encoding = encodedValueOf(program, property);
|
|
419
|
+
switch (encoding.kind) {
|
|
420
|
+
case "declared": {
|
|
421
|
+
// A constrained union keeps the members its own keywords allow - see `constrainedMembersOf`.
|
|
422
|
+
const members = constrainedMembersOf(program, property, position);
|
|
423
|
+
const inner = nullableInnerOf(property.type);
|
|
424
|
+
// A `null` a declaration above refuses is left out, as the validator leaves it out.
|
|
425
|
+
if (members === undefined) {
|
|
426
|
+
const declared = position.nonNull && inner !== undefined ? inner : property.type;
|
|
427
|
+
// A property's `@oneOf` makes its inline union exclusive, as the Zod walk reads it.
|
|
428
|
+
return withExclusiveSite(exclusiveSiteOf(program, property, declared), () => typeToTs(program, declared));
|
|
429
|
+
}
|
|
430
|
+
const text = members.length === 0
|
|
431
|
+
? UNREPRESENTABLE
|
|
432
|
+
: members
|
|
433
|
+
.map((member) => isTypelessMember(program, member) ? `(${ANY_JSON_TS})` : typeToTs(program, member))
|
|
434
|
+
.join(" | ");
|
|
435
|
+
return inner === undefined || position.nonNull ? text : orNull(text, true);
|
|
436
|
+
}
|
|
437
|
+
case "unsatisfiable":
|
|
438
|
+
return UNREPRESENTABLE;
|
|
439
|
+
case "replaced":
|
|
440
|
+
return orNull(typeToTs(program, encoding.scalar), encoding.nullable && !position.nonNull);
|
|
441
|
+
case "both": {
|
|
442
|
+
const member = (each) => replacesFormOf(program, encoding, each)
|
|
443
|
+
? typeToTs(program, encoding.scalar)
|
|
444
|
+
: typeToTs(program, each);
|
|
445
|
+
const kept = Array.isArray(encoding.kept)
|
|
446
|
+
? encoding.kept.map(member).join(" | ")
|
|
447
|
+
: member(encoding.kept);
|
|
448
|
+
const value = encodedIntersects(program, property, encoding)
|
|
449
|
+
? `${typeToTs(program, encoding.scalar)} & (${kept})`
|
|
450
|
+
: kept;
|
|
451
|
+
return orNull(value, encoding.nullable);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function orNull(text, nullable) {
|
|
456
|
+
return nullable ? `${text} | null` : text;
|
|
298
457
|
}
|
|
299
|
-
export function propertyToTs(program, property, wireName) {
|
|
458
|
+
export function propertyToTs(program, property, wireName, position = OWN_POSITION) {
|
|
300
459
|
// The WIRE name, resolved exactly as `zod.ts` resolves it - `wire-contract.gen.ts` asserts the
|
|
301
460
|
// emitted Zod infers this type, so the two must name every property identically or it fails.
|
|
302
461
|
const key = objectKey(wireName ?? resolveEncodedName(program, property, "application/json"));
|
|
303
|
-
const value = propertyTypeToTs(program, property);
|
|
462
|
+
const value = propertyTypeToTs(program, property, position);
|
|
304
463
|
/**
|
|
305
464
|
* **A defaulted property used to be REQUIRED here, and that was the OUTPUT type's answer to a
|
|
306
465
|
* question this file does not ask.**
|
|
@@ -356,9 +515,12 @@ export function propertyToTs(program, property, wireName) {
|
|
|
356
515
|
* what `propertyToZod` has always consulted. This walk read `property.optional` alone, so an
|
|
357
516
|
* update contract demanded every field while the validator beside it accepted any subset.
|
|
358
517
|
*/
|
|
359
|
-
if (!(property.optional || isOptionalAt(program, property)))
|
|
518
|
+
if (position.required || !(property.optional || isOptionalAt(program, property))) {
|
|
360
519
|
return `${key}: ${value};`;
|
|
361
|
-
|
|
520
|
+
}
|
|
521
|
+
return emitsDefault(program, property, position)
|
|
522
|
+
? `${key}?: ${value} | undefined;`
|
|
523
|
+
: `${key}?: ${value};`;
|
|
362
524
|
}
|
|
363
525
|
/** Installed by the registry so a named model is declared once and referenced everywhere else. */
|
|
364
526
|
let resolveRef = () => undefined;
|
|
@@ -386,8 +548,13 @@ export function typeToTsBody(program, type) {
|
|
|
386
548
|
switch (type.kind) {
|
|
387
549
|
case "Scalar":
|
|
388
550
|
return scalarToTs(program, type);
|
|
389
|
-
case "Model":
|
|
551
|
+
case "Model": {
|
|
552
|
+
// Refused as the schema walk refuses it - see `listTemplateInstanceWhy`.
|
|
553
|
+
const why = listTemplateInstanceWhy(program, type);
|
|
554
|
+
if (why !== undefined)
|
|
555
|
+
return refuse(program, type, why, LIST_TEMPLATE_REMEDY);
|
|
390
556
|
return modelToTs(program, type);
|
|
557
|
+
}
|
|
391
558
|
case "Enum":
|
|
392
559
|
return enumToTs(type);
|
|
393
560
|
case "Union":
|
|
@@ -409,9 +576,17 @@ export function typeToTsBody(program, type) {
|
|
|
409
576
|
if (type.name === "unknown")
|
|
410
577
|
return "unknown";
|
|
411
578
|
if (type.name === "void" || type.name === "never") {
|
|
412
|
-
return refuse(program, type, `\`${type.name}\` has no runtime representation`, "Give the position a type a value can inhabit. A `never`
|
|
579
|
+
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.");
|
|
413
580
|
}
|
|
414
581
|
return refuse(program, type, `intrinsic "${type.name}"`, "This emitter has no rule for that intrinsic. Replace it with a declared scalar or model.");
|
|
582
|
+
// `z.input` of the schema walk's `z.tuple([...exactOptional], z.unknown())`.
|
|
583
|
+
case "Tuple": {
|
|
584
|
+
if (inParameterPosition()) {
|
|
585
|
+
return refuse(program, type, TUPLE_PARAMETER.why, TUPLE_PARAMETER.remedy);
|
|
586
|
+
}
|
|
587
|
+
const positions = type.values.map((value) => `(${typeToTs(program, value)})?`);
|
|
588
|
+
return `[${[...positions, "...unknown[]"].join(", ")}]`;
|
|
589
|
+
}
|
|
415
590
|
default:
|
|
416
591
|
return refuse(program, type, "no rule for this kind", "Replace it with a model, scalar, enum or union, which are the kinds this emitter walks.");
|
|
417
592
|
}
|