@kubb/plugin-zod 5.0.0-beta.98 → 5.0.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/dist/index.cjs +323 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +324 -137
- package/dist/index.js.map +1 -1
- package/package.json +3 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,180 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-C0LytTxp.js";
|
|
2
|
-
import { Resolver, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
|
|
2
|
+
import { Resolver, ast, containsCircularRef, createResolver, defineGenerator, definePlugin, extractRefName, syncSchemaRef } from "kubb/kit";
|
|
3
3
|
import { Const, File, Type, jsxRenderer } from "kubb/jsx";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
|
|
5
|
+
//#region ../../internals/shared/src/params.ts
|
|
6
|
+
/**
|
|
7
|
+
* Drops parameters that share the same name, keeping the first.
|
|
8
|
+
*
|
|
9
|
+
* A malformed spec can declare the same parameter name twice within one `in` location. Both would
|
|
10
|
+
* resolve to the same output property, so emitting both would yield an object type with a duplicate
|
|
11
|
+
* member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
|
|
12
|
+
* parameter names flow through unchanged, so no two distinct names ever collide here anymore.
|
|
13
|
+
*/
|
|
14
|
+
function dedupeParams(params) {
|
|
15
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16
|
+
return params.filter((param) => {
|
|
17
|
+
if (seen.has(param.name)) return false;
|
|
18
|
+
seen.add(param.name);
|
|
19
|
+
return true;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region ../../internals/shared/src/operation.ts
|
|
24
|
+
/**
|
|
25
|
+
* Maps a content type to the PascalCase suffix used to name per-content-type variants
|
|
26
|
+
* (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
|
|
27
|
+
*/
|
|
28
|
+
function getContentTypeSuffix(contentType) {
|
|
29
|
+
const baseType = contentType.split(";")[0].trim();
|
|
30
|
+
if (baseType === "application/json") return "Json";
|
|
31
|
+
if (baseType === "multipart/form-data") return "FormData";
|
|
32
|
+
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
|
|
33
|
+
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
34
|
+
if (parts.length === 0) return "Unknown";
|
|
35
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
|
|
39
|
+
* (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
|
|
40
|
+
*/
|
|
41
|
+
function getPerContentTypeName(baseName, suffix) {
|
|
42
|
+
if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
|
|
43
|
+
return baseName + suffix;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolves per-content-type variant names for a set of content entries, deduplicating suffix
|
|
47
|
+
* collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
|
|
48
|
+
* the final (possibly counter-augmented) value, so callers can derive parallel names in another
|
|
49
|
+
* namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
|
|
50
|
+
*/
|
|
51
|
+
function resolveContentTypeVariants(entries, baseName) {
|
|
52
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
53
|
+
return entries.filter((entry) => entry.schema).map((entry) => {
|
|
54
|
+
const baseSuffix = getContentTypeSuffix(entry.contentType);
|
|
55
|
+
let suffix = baseSuffix;
|
|
56
|
+
let name = getPerContentTypeName(baseName, suffix);
|
|
57
|
+
let counter = 2;
|
|
58
|
+
while (usedNames.has(name)) {
|
|
59
|
+
suffix = `${baseSuffix}${counter++}`;
|
|
60
|
+
name = getPerContentTypeName(baseName, suffix);
|
|
61
|
+
}
|
|
62
|
+
usedNames.add(name);
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
suffix,
|
|
66
|
+
schema: entry.schema,
|
|
67
|
+
keysToOmit: entry.keysToOmit,
|
|
68
|
+
contentType: entry.contentType
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
|
|
73
|
+
/**
|
|
74
|
+
* Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
|
|
75
|
+
* group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
|
|
76
|
+
* (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
|
|
77
|
+
* same parameters once per plugin.
|
|
78
|
+
*/
|
|
79
|
+
function getOperationParameters(node) {
|
|
80
|
+
const cached = operationParameterGroupsByNode.get(node);
|
|
81
|
+
if (cached) return cached;
|
|
82
|
+
const groups = {
|
|
83
|
+
path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
|
|
84
|
+
query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
|
|
85
|
+
header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
|
|
86
|
+
cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
|
|
87
|
+
};
|
|
88
|
+
operationParameterGroupsByNode.set(node, groups);
|
|
89
|
+
return groups;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Builds the combined `{ body, path, query, headers }` options object schema for an operation,
|
|
93
|
+
* referencing the already-resolved body and grouped param names. Shared by `@kubb/plugin-ts`'s
|
|
94
|
+
* `Options` type and `@kubb/plugin-zod`'s inferred options schema, so both printers emit the same
|
|
95
|
+
* shape from the same inputs. `primitive: 'object'` is a no-op for the TS printer and tells the Zod
|
|
96
|
+
* printer to emit `z.object(…)` rather than a record.
|
|
97
|
+
*/
|
|
98
|
+
function buildOptionsSchema(node, resolver) {
|
|
99
|
+
const { path, query, header } = getOperationParameters(node);
|
|
100
|
+
const hasBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
101
|
+
const createNever = () => ast.factory.createSchema({
|
|
102
|
+
type: "never",
|
|
103
|
+
primitive: void 0,
|
|
104
|
+
optional: true
|
|
105
|
+
});
|
|
106
|
+
const groups = [
|
|
107
|
+
{
|
|
108
|
+
name: "path",
|
|
109
|
+
params: path,
|
|
110
|
+
resolve: resolver.param.path
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "query",
|
|
114
|
+
params: query,
|
|
115
|
+
resolve: resolver.param.query
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "headers",
|
|
119
|
+
params: header,
|
|
120
|
+
resolve: resolver.param.headers
|
|
121
|
+
}
|
|
122
|
+
];
|
|
123
|
+
return ast.factory.createSchema({
|
|
124
|
+
type: "object",
|
|
125
|
+
primitive: "object",
|
|
126
|
+
deprecated: node.deprecated,
|
|
127
|
+
properties: [ast.factory.createProperty({
|
|
128
|
+
name: "body",
|
|
129
|
+
required: hasBody,
|
|
130
|
+
schema: hasBody ? ast.factory.createSchema({
|
|
131
|
+
type: "ref",
|
|
132
|
+
name: resolver.response.body(node)
|
|
133
|
+
}) : createNever()
|
|
134
|
+
}), ...groups.map(({ name, params, resolve }) => {
|
|
135
|
+
const required = params.some((param) => param.required);
|
|
136
|
+
return ast.factory.createProperty({
|
|
137
|
+
name,
|
|
138
|
+
required,
|
|
139
|
+
schema: params.length > 0 ? ast.factory.createSchema({
|
|
140
|
+
type: "ref",
|
|
141
|
+
name: resolve.call(resolver.param, node, params[0]),
|
|
142
|
+
optional: !required
|
|
143
|
+
}) : createNever()
|
|
144
|
+
});
|
|
145
|
+
})]
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function getStatusCodeNumber(statusCode) {
|
|
149
|
+
const code = Number(statusCode);
|
|
150
|
+
return Number.isNaN(code) ? null : code;
|
|
151
|
+
}
|
|
152
|
+
function isSuccessStatusCode(statusCode) {
|
|
153
|
+
const code = getStatusCodeNumber(statusCode);
|
|
154
|
+
return code !== null && code >= 200 && code < 300;
|
|
155
|
+
}
|
|
156
|
+
function getSuccessResponses(responses) {
|
|
157
|
+
return responses.filter((response) => isSuccessStatusCode(response.statusCode));
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region ../../internals/shared/src/adapter.ts
|
|
161
|
+
/**
|
|
162
|
+
* Narrows the generic `Adapter` from a generator context to the OpenAPI adapter,
|
|
163
|
+
* so OAS-only options (`dateType`, `enums`) and the parsed `document` are typed.
|
|
164
|
+
*
|
|
165
|
+
* Throws when a non-OAS adapter is configured, turning a silently wrong cast into a
|
|
166
|
+
* clear, actionable error at the point of use.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* const { dateType } = getOasAdapter(ctx.adapter).options
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
function getOasAdapter(adapter) {
|
|
174
|
+
if (adapter.name !== "oas") throw new Error(`Expected the OpenAPI adapter (adapterOas), but received "${adapter.name}". Configure \`adapter: adapterOas()\` in your Kubb config.`);
|
|
175
|
+
return adapter;
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
5
178
|
//#region ../../internals/utils/src/casing.ts
|
|
6
179
|
/**
|
|
7
180
|
* Shared implementation for camelCase and PascalCase conversion.
|
|
@@ -346,106 +519,6 @@ function toFilePath(name, caseLast = camelCase) {
|
|
|
346
519
|
return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
|
|
347
520
|
}
|
|
348
521
|
//#endregion
|
|
349
|
-
//#region ../../internals/shared/src/params.ts
|
|
350
|
-
const caseParamsCache = /* @__PURE__ */ new WeakMap();
|
|
351
|
-
/**
|
|
352
|
-
* Applies camelCase to parameter names and returns a new array without mutating the input.
|
|
353
|
-
*
|
|
354
|
-
* Run it before handing parameters to schema builders so output property keys get the right casing
|
|
355
|
-
* while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
|
|
356
|
-
* original array is returned unchanged. Results are cached per input array.
|
|
357
|
-
*/
|
|
358
|
-
function caseParams(params, casing) {
|
|
359
|
-
if (!casing) return params;
|
|
360
|
-
const cached = caseParamsCache.get(params);
|
|
361
|
-
if (cached) return cached;
|
|
362
|
-
const result = params.map((param) => ({
|
|
363
|
-
...param,
|
|
364
|
-
name: camelCase(param.name)
|
|
365
|
-
}));
|
|
366
|
-
caseParamsCache.set(params, result);
|
|
367
|
-
return result;
|
|
368
|
-
}
|
|
369
|
-
//#endregion
|
|
370
|
-
//#region ../../internals/shared/src/operation.ts
|
|
371
|
-
/**
|
|
372
|
-
* Maps a content type to the PascalCase suffix used to name per-content-type variants
|
|
373
|
-
* (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
|
|
374
|
-
*/
|
|
375
|
-
function getContentTypeSuffix(contentType) {
|
|
376
|
-
const baseType = contentType.split(";")[0].trim();
|
|
377
|
-
if (baseType === "application/json") return "Json";
|
|
378
|
-
if (baseType === "multipart/form-data") return "FormData";
|
|
379
|
-
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
|
|
380
|
-
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
381
|
-
if (parts.length === 0) return "Unknown";
|
|
382
|
-
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
|
|
386
|
-
* (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
|
|
387
|
-
*/
|
|
388
|
-
function getPerContentTypeName(baseName, suffix) {
|
|
389
|
-
if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
|
|
390
|
-
return baseName + suffix;
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Resolves per-content-type variant names for a set of content entries, deduplicating suffix
|
|
394
|
-
* collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
|
|
395
|
-
* the final (possibly counter-augmented) value, so callers can derive parallel names in another
|
|
396
|
-
* namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
|
|
397
|
-
*/
|
|
398
|
-
function resolveContentTypeVariants(entries, baseName) {
|
|
399
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
400
|
-
return entries.filter((entry) => entry.schema).map((entry) => {
|
|
401
|
-
const baseSuffix = getContentTypeSuffix(entry.contentType);
|
|
402
|
-
let suffix = baseSuffix;
|
|
403
|
-
let name = getPerContentTypeName(baseName, suffix);
|
|
404
|
-
let counter = 2;
|
|
405
|
-
while (usedNames.has(name)) {
|
|
406
|
-
suffix = `${baseSuffix}${counter++}`;
|
|
407
|
-
name = getPerContentTypeName(baseName, suffix);
|
|
408
|
-
}
|
|
409
|
-
usedNames.add(name);
|
|
410
|
-
return {
|
|
411
|
-
name,
|
|
412
|
-
suffix,
|
|
413
|
-
schema: entry.schema,
|
|
414
|
-
keysToOmit: entry.keysToOmit,
|
|
415
|
-
contentType: entry.contentType
|
|
416
|
-
};
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
function getStatusCodeNumber(statusCode) {
|
|
420
|
-
const code = Number(statusCode);
|
|
421
|
-
return Number.isNaN(code) ? null : code;
|
|
422
|
-
}
|
|
423
|
-
function isSuccessStatusCode(statusCode) {
|
|
424
|
-
const code = getStatusCodeNumber(statusCode);
|
|
425
|
-
return code !== null && code >= 200 && code < 300;
|
|
426
|
-
}
|
|
427
|
-
function getSuccessResponses(responses) {
|
|
428
|
-
return responses.filter((response) => isSuccessStatusCode(response.statusCode));
|
|
429
|
-
}
|
|
430
|
-
//#endregion
|
|
431
|
-
//#region ../../internals/shared/src/adapter.ts
|
|
432
|
-
/**
|
|
433
|
-
* Narrows the generic `Adapter` from a generator context to the OpenAPI adapter,
|
|
434
|
-
* so OAS-only options (`dateType`, `enums`) and the parsed `document` are typed.
|
|
435
|
-
*
|
|
436
|
-
* Throws when a non-OAS adapter is configured, turning a silently wrong cast into a
|
|
437
|
-
* clear, actionable error at the point of use.
|
|
438
|
-
*
|
|
439
|
-
* @example
|
|
440
|
-
* ```ts
|
|
441
|
-
* const { dateType } = getOasAdapter(ctx.adapter).options
|
|
442
|
-
* ```
|
|
443
|
-
*/
|
|
444
|
-
function getOasAdapter(adapter) {
|
|
445
|
-
if (adapter.name !== "oas") throw new Error(`Expected the OpenAPI adapter (adapterOas), but received "${adapter.name}". Configure \`adapter: adapterOas()\` in your Kubb config.`);
|
|
446
|
-
return adapter;
|
|
447
|
-
}
|
|
448
|
-
//#endregion
|
|
449
522
|
//#region ../../internals/shared/src/resolver.ts
|
|
450
523
|
/**
|
|
451
524
|
* Resolves a single operation parameter name with the
|
|
@@ -458,6 +531,29 @@ function operationParamName(node, param) {
|
|
|
458
531
|
return this.name(`${node.operationId} ${param.in} ${param.name}`);
|
|
459
532
|
}
|
|
460
533
|
/**
|
|
534
|
+
* Builds the shared `param` namespace. Spread the result into `createResolver`
|
|
535
|
+
* and override individual methods next to it when a plugin deviates.
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```ts
|
|
539
|
+
* createResolver<PluginTs>({ param: createOperationParamResolver(), ... })
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
function createOperationParamResolver() {
|
|
543
|
+
return {
|
|
544
|
+
name: operationParamName,
|
|
545
|
+
path(node) {
|
|
546
|
+
return this.name(`${node.operationId} Path`);
|
|
547
|
+
},
|
|
548
|
+
query(node) {
|
|
549
|
+
return this.name(`${node.operationId} Query`);
|
|
550
|
+
},
|
|
551
|
+
headers(node) {
|
|
552
|
+
return this.name(`${node.operationId} Headers`);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
461
557
|
* Builds the shared `response` namespace. Spread the result into
|
|
462
558
|
* `createResolver` and add plugin-specific methods (`options`, `error`) next
|
|
463
559
|
* to it.
|
|
@@ -505,7 +601,7 @@ function createCasedFile(caseLast) {
|
|
|
505
601
|
* `resolver.imports` would resolve file paths that are then discarded.
|
|
506
602
|
*/
|
|
507
603
|
function collectRefNames(schema) {
|
|
508
|
-
return ast.
|
|
604
|
+
return ast.collectSync(schema, { schema: (node) => {
|
|
509
605
|
const refNode = ast.narrowSchema(node, "ref");
|
|
510
606
|
if (!refNode?.ref) return null;
|
|
511
607
|
return ast.resolveRefName(refNode);
|
|
@@ -543,6 +639,45 @@ function createGroupConfig(group) {
|
|
|
543
639
|
};
|
|
544
640
|
}
|
|
545
641
|
//#endregion
|
|
642
|
+
//#region ../../internals/shared/src/schemaTraversal.ts
|
|
643
|
+
/**
|
|
644
|
+
* Maps each property of an object schema to its transformed output. Pairs every result with the
|
|
645
|
+
* original property so the printer keeps full control over modifiers, getters, and key syntax.
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```ts
|
|
649
|
+
* const entries = mapSchemaProperties(node, (schema) => this.transform(schema))
|
|
650
|
+
* // entries: [{ name: 'id', property, output: 'z.number()' }, ...]
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
function mapSchemaProperties(node, transform) {
|
|
654
|
+
return node.properties.map((property) => ({
|
|
655
|
+
name: property.name,
|
|
656
|
+
property,
|
|
657
|
+
output: transform(property.schema)
|
|
658
|
+
}));
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Maps each member of a union or intersection schema to its transformed output, pairing every
|
|
662
|
+
* result with the original member.
|
|
663
|
+
*/
|
|
664
|
+
function mapSchemaMembers(node, transform) {
|
|
665
|
+
return (node.members ?? []).map((schema) => ({
|
|
666
|
+
schema,
|
|
667
|
+
output: transform(schema)
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Maps each item of an array or tuple schema to its transformed output, pairing every result with
|
|
672
|
+
* the original item.
|
|
673
|
+
*/
|
|
674
|
+
function mapSchemaItems(node, transform) {
|
|
675
|
+
return (node.items ?? []).map((schema) => ({
|
|
676
|
+
schema,
|
|
677
|
+
output: transform(schema)
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
546
681
|
//#region src/components/Zod.tsx
|
|
547
682
|
function Zod({ name, node, printer, inferTypeName, cyclic }) {
|
|
548
683
|
const output = printer.print(node);
|
|
@@ -626,12 +761,12 @@ function containsCodec(node, seen = /* @__PURE__ */ new Set()) {
|
|
|
626
761
|
if (hasCodec(node)) return true;
|
|
627
762
|
if (node.type === "ref") {
|
|
628
763
|
if (!node.ref) return false;
|
|
629
|
-
const refName =
|
|
764
|
+
const refName = extractRefName(node.ref);
|
|
630
765
|
if (refName) {
|
|
631
766
|
if (seen.has(refName)) return false;
|
|
632
767
|
seen.add(refName);
|
|
633
768
|
}
|
|
634
|
-
const resolved =
|
|
769
|
+
const resolved = syncSchemaRef(node);
|
|
635
770
|
if (resolved.type === "ref") return false;
|
|
636
771
|
return containsCodec(resolved, seen);
|
|
637
772
|
}
|
|
@@ -647,7 +782,7 @@ function containsCodec(node, seen = /* @__PURE__ */ new Set()) {
|
|
|
647
782
|
* them to their input (encode) variant.
|
|
648
783
|
*/
|
|
649
784
|
function collectCodecRefNames(node) {
|
|
650
|
-
return ast.
|
|
785
|
+
return ast.collectSync(node, { schema: (n) => n.type === "ref" && n.ref && containsCodec(n) ? ast.resolveRefName(n) ?? void 0 : void 0 });
|
|
651
786
|
}
|
|
652
787
|
/**
|
|
653
788
|
* Whether the node is a plain inline object whose shape can be lifted into an `.extend({ … })`
|
|
@@ -670,7 +805,7 @@ function isObjectSchemaNode(node, cyclicSchemas) {
|
|
|
670
805
|
if (node.type === "ref") {
|
|
671
806
|
const refName = ast.resolveRefName(node);
|
|
672
807
|
if (refName && cyclicSchemas?.has(refName)) return false;
|
|
673
|
-
const resolved =
|
|
808
|
+
const resolved = syncSchemaRef(node);
|
|
674
809
|
return resolved.type === "ref" || isObjectSchemaNode(resolved, cyclicSchemas);
|
|
675
810
|
}
|
|
676
811
|
if (node.type === "union") {
|
|
@@ -746,6 +881,20 @@ function buildEnum(values) {
|
|
|
746
881
|
return `z.union([${literals.join(", ")}])`;
|
|
747
882
|
}
|
|
748
883
|
/**
|
|
884
|
+
* Digit pattern for a `type: 'string'` schema that carries an integer `format`, the way ProtoJSON
|
|
885
|
+
* encodes 64-bit integers. Returns `undefined` for every other format, and a `pattern` from the
|
|
886
|
+
* spec takes precedence over this fallback.
|
|
887
|
+
*
|
|
888
|
+
* @example
|
|
889
|
+
* ```ts
|
|
890
|
+
* integerFormatPattern('uint64') // '^\\d+$'
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
function integerFormatPattern(format) {
|
|
894
|
+
if (format === "int32" || format === "int64") return "^-?\\d+$";
|
|
895
|
+
if (format === "uint64") return "^\\d+$";
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
749
898
|
* Map a `regexType` to the `func` argument of `toRegExpString`: `'constructor'` emits
|
|
750
899
|
* `new RegExp(...)`, while `'literal'` (the default) emits a regex literal.
|
|
751
900
|
*/
|
|
@@ -870,6 +1019,22 @@ function applyMiniModifiers({ value, schema, nullable, optional, nullish, defaul
|
|
|
870
1019
|
const literal = defaultValue !== void 0 ? defaultLiteral(schema, defaultValue) : null;
|
|
871
1020
|
return literal !== null ? `z._default(${withModifier}, ${literal})` : withModifier;
|
|
872
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Builds an `object` schema node grouping the given parameter nodes.
|
|
1024
|
+
* The `primitive: 'object'` marker ensures the Zod printer emits `z.object(…)` rather than a record.
|
|
1025
|
+
*/
|
|
1026
|
+
function buildGroupedParamsSchema({ params, optional }) {
|
|
1027
|
+
return ast.factory.createSchema({
|
|
1028
|
+
type: "object",
|
|
1029
|
+
optional,
|
|
1030
|
+
primitive: "object",
|
|
1031
|
+
properties: params.map((param) => ast.factory.createProperty({
|
|
1032
|
+
name: param.name,
|
|
1033
|
+
required: param.required,
|
|
1034
|
+
schema: param.schema
|
|
1035
|
+
}))
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
873
1038
|
//#endregion
|
|
874
1039
|
//#region src/printers/printerZod.ts
|
|
875
1040
|
function strictOneOfMember$1(member, node, cyclicSchemas) {
|
|
@@ -878,7 +1043,7 @@ function strictOneOfMember$1(member, node, cyclicSchemas) {
|
|
|
878
1043
|
if (member.startsWith("z.lazy(")) return member;
|
|
879
1044
|
const refName = ast.resolveRefName(node);
|
|
880
1045
|
if (refName && cyclicSchemas?.has(refName)) return member;
|
|
881
|
-
const schema =
|
|
1046
|
+
const schema = syncSchemaRef(node);
|
|
882
1047
|
if (schema.nullable || schema.optional || node.nullable || node.optional) return member;
|
|
883
1048
|
if (schema.type === "object" && (schema.additionalProperties === void 0 || schema.additionalProperties === false)) return `${member}.strict()`;
|
|
884
1049
|
}
|
|
@@ -903,8 +1068,8 @@ function getMemberConstraint({ member, regexType }) {
|
|
|
903
1068
|
function buildZodObjectShape(ctx, node) {
|
|
904
1069
|
const objectNode = ast.narrowSchema(node, "object");
|
|
905
1070
|
if (!objectNode) return "{}";
|
|
906
|
-
const isCyclic = (schema) => ctx.options.cyclicSchemas != null &&
|
|
907
|
-
return buildObject(
|
|
1071
|
+
const isCyclic = (schema) => ctx.options.cyclicSchemas != null && containsCircularRef(schema, { circularSchemas: ctx.options.cyclicSchemas });
|
|
1072
|
+
return buildObject(mapSchemaProperties(objectNode, (schema) => {
|
|
908
1073
|
const hasSelfRef = isCyclic(schema);
|
|
909
1074
|
const savedCyclicSchemas = ctx.options.cyclicSchemas;
|
|
910
1075
|
if (hasSelfRef) ctx.options.cyclicSchemas = void 0;
|
|
@@ -913,7 +1078,7 @@ function buildZodObjectShape(ctx, node) {
|
|
|
913
1078
|
return baseOutput;
|
|
914
1079
|
}).map(({ name: propName, property, output: baseOutput }) => {
|
|
915
1080
|
const { schema } = property;
|
|
916
|
-
const meta =
|
|
1081
|
+
const meta = syncSchemaRef(schema);
|
|
917
1082
|
const descriptionToApply = schema.type !== "ref" && meta.type === "ref" ? void 0 : meta.description;
|
|
918
1083
|
const value = applyModifiers({
|
|
919
1084
|
value: baseOutput,
|
|
@@ -956,8 +1121,11 @@ const printerZod = ast.createPrinter((options) => {
|
|
|
956
1121
|
boolean: () => "z.boolean()",
|
|
957
1122
|
null: () => "z.null()",
|
|
958
1123
|
string(node) {
|
|
959
|
-
|
|
1124
|
+
const base = shouldCoerce(this.options.coercion, "strings") ? "z.coerce.string()" : "z.string()";
|
|
1125
|
+
const pattern = node.pattern ?? integerFormatPattern(node.format);
|
|
1126
|
+
return `${base}${lengthConstraints({
|
|
960
1127
|
...node,
|
|
1128
|
+
pattern,
|
|
961
1129
|
regexType: this.options.regexType
|
|
962
1130
|
})}`;
|
|
963
1131
|
},
|
|
@@ -1056,18 +1224,18 @@ const printerZod = ast.createPrinter((options) => {
|
|
|
1056
1224
|
})();
|
|
1057
1225
|
},
|
|
1058
1226
|
array(node) {
|
|
1059
|
-
const base = `z.array(${
|
|
1227
|
+
const base = `z.array(${mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: "unknown" }))})${lengthConstraints({
|
|
1060
1228
|
...node,
|
|
1061
1229
|
regexType: this.options.regexType
|
|
1062
1230
|
})}`;
|
|
1063
1231
|
return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base;
|
|
1064
1232
|
},
|
|
1065
1233
|
tuple(node) {
|
|
1066
|
-
return `z.tuple(${buildList(
|
|
1234
|
+
return `z.tuple(${buildList(mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean))})`;
|
|
1067
1235
|
},
|
|
1068
1236
|
union(node) {
|
|
1069
1237
|
const nodeMembers = node.members ?? [];
|
|
1070
|
-
const members =
|
|
1238
|
+
const members = mapSchemaMembers(node, (memberNode) => this.transform(memberNode)).map(({ schema, output }) => output && node.strategy === "one" ? strictOneOfMember$1(output, schema, cyclicSchemaNames) : output).filter(Boolean);
|
|
1071
1239
|
if (members.length === 0) return "";
|
|
1072
1240
|
if (members.length === 1) return members[0];
|
|
1073
1241
|
const allDiscriminable = nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemaNames));
|
|
@@ -1098,7 +1266,7 @@ const printerZod = ast.createPrinter((options) => {
|
|
|
1098
1266
|
const { keysToOmit } = this.options;
|
|
1099
1267
|
const transformed = this.transform(node);
|
|
1100
1268
|
if (!transformed) return null;
|
|
1101
|
-
const meta =
|
|
1269
|
+
const meta = syncSchemaRef(node);
|
|
1102
1270
|
return applyModifiers({
|
|
1103
1271
|
value: (() => {
|
|
1104
1272
|
if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
|
|
@@ -1143,8 +1311,8 @@ function getMemberConstraintMini({ member, regexType }) {
|
|
|
1143
1311
|
function buildZodMiniObjectShape(ctx, node) {
|
|
1144
1312
|
const objectNode = ast.narrowSchema(node, "object");
|
|
1145
1313
|
if (!objectNode) return "{}";
|
|
1146
|
-
const isCyclic = (schema) => ctx.options.cyclicSchemas != null &&
|
|
1147
|
-
return buildObject(
|
|
1314
|
+
const isCyclic = (schema) => ctx.options.cyclicSchemas != null && containsCircularRef(schema, { circularSchemas: ctx.options.cyclicSchemas });
|
|
1315
|
+
return buildObject(mapSchemaProperties(objectNode, (schema) => {
|
|
1148
1316
|
const hasSelfRef = isCyclic(schema);
|
|
1149
1317
|
const savedCyclicSchemas = ctx.options.cyclicSchemas;
|
|
1150
1318
|
if (hasSelfRef) ctx.options.cyclicSchemas = void 0;
|
|
@@ -1153,7 +1321,7 @@ function buildZodMiniObjectShape(ctx, node) {
|
|
|
1153
1321
|
return baseOutput;
|
|
1154
1322
|
}).map(({ name: propName, property, output: baseOutput }) => {
|
|
1155
1323
|
const { schema } = property;
|
|
1156
|
-
const meta =
|
|
1324
|
+
const meta = syncSchemaRef(schema);
|
|
1157
1325
|
const value = applyMiniModifiers({
|
|
1158
1326
|
value: baseOutput,
|
|
1159
1327
|
schema,
|
|
@@ -1193,8 +1361,10 @@ const printerZodMini = ast.createPrinter((options) => {
|
|
|
1193
1361
|
boolean: () => "z.boolean()",
|
|
1194
1362
|
null: () => "z.null()",
|
|
1195
1363
|
string(node) {
|
|
1364
|
+
const pattern = node.pattern ?? integerFormatPattern(node.format);
|
|
1196
1365
|
return `z.string()${lengthChecksMini({
|
|
1197
1366
|
...node,
|
|
1367
|
+
pattern,
|
|
1198
1368
|
regexType: this.options.regexType
|
|
1199
1369
|
})}`;
|
|
1200
1370
|
},
|
|
@@ -1282,18 +1452,18 @@ const printerZodMini = ast.createPrinter((options) => {
|
|
|
1282
1452
|
return objectBase;
|
|
1283
1453
|
},
|
|
1284
1454
|
array(node) {
|
|
1285
|
-
const base = `z.array(${
|
|
1455
|
+
const base = `z.array(${mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: "unknown" }))})${lengthChecksMini({
|
|
1286
1456
|
...node,
|
|
1287
1457
|
regexType: this.options.regexType
|
|
1288
1458
|
})}`;
|
|
1289
1459
|
return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base;
|
|
1290
1460
|
},
|
|
1291
1461
|
tuple(node) {
|
|
1292
|
-
return `z.tuple(${buildList(
|
|
1462
|
+
return `z.tuple(${buildList(mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean))})`;
|
|
1293
1463
|
},
|
|
1294
1464
|
union(node) {
|
|
1295
1465
|
const nodeMembers = node.members ?? [];
|
|
1296
|
-
const members =
|
|
1466
|
+
const members = mapSchemaMembers(node, (memberNode) => this.transform(memberNode)).map(({ schema, output }) => output && node.strategy === "one" ? strictOneOfMember(output, schema) : output).filter(Boolean);
|
|
1297
1467
|
if (members.length === 0) return "";
|
|
1298
1468
|
if (members.length === 1) return members[0];
|
|
1299
1469
|
const allDiscriminable = nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemaNames));
|
|
@@ -1324,7 +1494,7 @@ const printerZodMini = ast.createPrinter((options) => {
|
|
|
1324
1494
|
const { keysToOmit } = this.options;
|
|
1325
1495
|
const transformed = this.transform(node);
|
|
1326
1496
|
if (!transformed) return null;
|
|
1327
|
-
const meta =
|
|
1497
|
+
const meta = syncSchemaRef(node);
|
|
1328
1498
|
return applyMiniModifiers({
|
|
1329
1499
|
value: (() => {
|
|
1330
1500
|
if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
|
|
@@ -1521,7 +1691,6 @@ const zodGenerator = defineGenerator({
|
|
|
1521
1691
|
const { output, coercion, guidType, regexType, mini, inferred, importPath, group, printer } = ctx.options;
|
|
1522
1692
|
const dateType = getOasAdapter(adapter).options.dateType;
|
|
1523
1693
|
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
|
|
1524
|
-
const params = caseParams(node.parameters, "camelcase");
|
|
1525
1694
|
const meta = { file: resolver.file({
|
|
1526
1695
|
name: node.operationId,
|
|
1527
1696
|
extname: ".ts",
|
|
@@ -1627,7 +1796,7 @@ const zodGenerator = defineGenerator({
|
|
|
1627
1796
|
name
|
|
1628
1797
|
});
|
|
1629
1798
|
}
|
|
1630
|
-
const paramSchemas =
|
|
1799
|
+
const paramSchemas = node.parameters.map((param) => renderSchemaEntry({
|
|
1631
1800
|
schema: param.schema,
|
|
1632
1801
|
name: resolver.param.name(node, param),
|
|
1633
1802
|
direction: "input"
|
|
@@ -1676,6 +1845,30 @@ const zodGenerator = defineGenerator({
|
|
|
1676
1845
|
description: node.requestBody.description ?? schema.description
|
|
1677
1846
|
}), "input");
|
|
1678
1847
|
})();
|
|
1848
|
+
const { path, query, header } = getOperationParameters(node);
|
|
1849
|
+
const paramGroupSchemas = inferred ? [
|
|
1850
|
+
{
|
|
1851
|
+
kind: "path",
|
|
1852
|
+
params: path
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
kind: "query",
|
|
1856
|
+
params: query
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
kind: "headers",
|
|
1860
|
+
params: header
|
|
1861
|
+
}
|
|
1862
|
+
].filter(({ params }) => params.length > 0).map(({ kind, params }) => renderSchemaEntry({
|
|
1863
|
+
schema: buildGroupedParamsSchema({ params }),
|
|
1864
|
+
name: resolver.param[kind](node, params[0]),
|
|
1865
|
+
direction: "input"
|
|
1866
|
+
})) : [];
|
|
1867
|
+
const optionsSchema = inferred ? renderSchemaEntry({
|
|
1868
|
+
schema: buildOptionsSchema(node, resolver),
|
|
1869
|
+
name: resolver.name(`${node.operationId} Options`),
|
|
1870
|
+
direction: "input"
|
|
1871
|
+
}) : null;
|
|
1679
1872
|
return /* @__PURE__ */ jsxs(File, {
|
|
1680
1873
|
baseName: meta.file.baseName,
|
|
1681
1874
|
path: meta.file.path,
|
|
@@ -1706,7 +1899,9 @@ const zodGenerator = defineGenerator({
|
|
|
1706
1899
|
responseSchemas,
|
|
1707
1900
|
responseUnionSchema,
|
|
1708
1901
|
errorUnionSchema,
|
|
1709
|
-
requestSchema
|
|
1902
|
+
requestSchema,
|
|
1903
|
+
paramGroupSchemas,
|
|
1904
|
+
optionsSchema
|
|
1710
1905
|
]
|
|
1711
1906
|
});
|
|
1712
1907
|
}
|
|
@@ -1748,22 +1943,14 @@ const resolverZod = createResolver({
|
|
|
1748
1943
|
return this.schema.typeName(`${name} input`);
|
|
1749
1944
|
}
|
|
1750
1945
|
},
|
|
1751
|
-
param:
|
|
1752
|
-
name: operationParamName,
|
|
1753
|
-
path(node, param) {
|
|
1754
|
-
return this.param.name(node, param);
|
|
1755
|
-
},
|
|
1756
|
-
query(node, param) {
|
|
1757
|
-
return this.param.name(node, param);
|
|
1758
|
-
},
|
|
1759
|
-
headers(node, param) {
|
|
1760
|
-
return this.param.name(node, param);
|
|
1761
|
-
}
|
|
1762
|
-
},
|
|
1946
|
+
param: createOperationParamResolver(),
|
|
1763
1947
|
response: {
|
|
1764
1948
|
...createOperationResponseResolver(),
|
|
1765
1949
|
error(node) {
|
|
1766
1950
|
return this.name(`${node.operationId} Error`);
|
|
1951
|
+
},
|
|
1952
|
+
options(node) {
|
|
1953
|
+
return this.schema.type(this.name(`${node.operationId} Options`));
|
|
1767
1954
|
}
|
|
1768
1955
|
}
|
|
1769
1956
|
});
|