@kubb/ast 5.0.0-alpha.4 → 5.0.0-alpha.40
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 +1682 -196
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3323 -15
- package/dist/index.js +1624 -187
- package/dist/index.js.map +1 -1
- package/package.json +3 -13
- package/src/constants.ts +127 -5
- package/src/factory.ts +662 -22
- package/src/guards.ts +77 -9
- package/src/index.ts +44 -6
- package/src/infer.ts +130 -0
- package/src/mocks.ts +18 -11
- package/src/nodes/base.ts +44 -4
- package/src/nodes/code.ts +304 -0
- package/src/nodes/file.ts +230 -0
- package/src/nodes/function.ts +219 -0
- package/src/nodes/http.ts +17 -5
- package/src/nodes/index.ts +48 -7
- package/src/nodes/operation.ts +69 -6
- package/src/nodes/output.ts +26 -0
- package/src/nodes/parameter.ts +27 -1
- package/src/nodes/property.ts +23 -1
- package/src/nodes/response.ts +29 -3
- package/src/nodes/root.ts +46 -14
- package/src/nodes/schema.ts +440 -42
- package/src/printer.ts +172 -60
- package/src/refs.ts +39 -7
- package/src/resolvers.ts +45 -0
- package/src/transformers.ts +156 -0
- package/src/types.ts +30 -4
- package/src/utils.ts +620 -4
- package/src/visitor.ts +373 -90
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +0 -2
- package/dist/types.js +0 -1
- package/dist/visitor-oFfdU8QA.d.ts +0 -653
package/dist/index.js
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import "./chunk--u3MIqq1.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
//#region src/constants.ts
|
|
3
5
|
const visitorDepths = {
|
|
4
6
|
shallow: "shallow",
|
|
5
7
|
deep: "deep"
|
|
6
8
|
};
|
|
7
9
|
const nodeKinds = {
|
|
8
|
-
|
|
10
|
+
input: "Input",
|
|
11
|
+
output: "Output",
|
|
9
12
|
operation: "Operation",
|
|
10
13
|
schema: "Schema",
|
|
11
14
|
property: "Property",
|
|
12
15
|
parameter: "Parameter",
|
|
13
|
-
response: "Response"
|
|
16
|
+
response: "Response",
|
|
17
|
+
functionParameter: "FunctionParameter",
|
|
18
|
+
parameterGroup: "ParameterGroup",
|
|
19
|
+
functionParameters: "FunctionParameters",
|
|
20
|
+
type: "Type",
|
|
21
|
+
file: "File",
|
|
22
|
+
import: "Import",
|
|
23
|
+
export: "Export",
|
|
24
|
+
source: "Source",
|
|
25
|
+
text: "Text",
|
|
26
|
+
break: "Break"
|
|
14
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Canonical schema type strings used by AST schema nodes.
|
|
30
|
+
*
|
|
31
|
+
* These values are used across the AST as stable discriminators
|
|
32
|
+
* (for example `schema.type === schemaTypes.object`).
|
|
33
|
+
*
|
|
34
|
+
* The map is grouped by intent:
|
|
35
|
+
* - primitives (`string`, `number`, `boolean`, ...)
|
|
36
|
+
* - structural/composite (`object`, `array`, `union`, ...)
|
|
37
|
+
* - special OpenAPI-oriented types (`ref`, `datetime`, `uuid`, ...)
|
|
38
|
+
*/
|
|
15
39
|
const schemaTypes = {
|
|
16
40
|
string: "string",
|
|
17
41
|
number: "number",
|
|
@@ -35,8 +59,27 @@ const schemaTypes = {
|
|
|
35
59
|
uuid: "uuid",
|
|
36
60
|
email: "email",
|
|
37
61
|
url: "url",
|
|
38
|
-
|
|
62
|
+
ipv4: "ipv4",
|
|
63
|
+
ipv6: "ipv6",
|
|
64
|
+
blob: "blob",
|
|
65
|
+
never: "never"
|
|
39
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Primitive scalar schema types used when simplifying union members.
|
|
69
|
+
*/
|
|
70
|
+
const SCALAR_PRIMITIVE_TYPES = new Set([
|
|
71
|
+
"string",
|
|
72
|
+
"number",
|
|
73
|
+
"integer",
|
|
74
|
+
"bigint",
|
|
75
|
+
"boolean"
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* Returns `true` when `type` is a scalar primitive schema type.
|
|
79
|
+
*/
|
|
80
|
+
function isScalarPrimitive(type) {
|
|
81
|
+
return SCALAR_PRIMITIVE_TYPES.has(type);
|
|
82
|
+
}
|
|
40
83
|
const httpMethods = {
|
|
41
84
|
get: "GET",
|
|
42
85
|
post: "POST",
|
|
@@ -69,204 +112,1353 @@ const mediaTypes = {
|
|
|
69
112
|
videoMp4: "video/mp4"
|
|
70
113
|
};
|
|
71
114
|
//#endregion
|
|
115
|
+
//#region ../../internals/utils/src/casing.ts
|
|
116
|
+
/**
|
|
117
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
118
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
119
|
+
* and capitalizes each word according to `pascal`.
|
|
120
|
+
*
|
|
121
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
122
|
+
*/
|
|
123
|
+
function toCamelOrPascal(text, pascal) {
|
|
124
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
125
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
126
|
+
if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
|
|
127
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
128
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Splits `text` on `.` and applies `transformPart` to each segment.
|
|
132
|
+
* The last segment receives `isLast = true`, all earlier segments receive `false`.
|
|
133
|
+
* Segments are joined with `/` to form a file path.
|
|
134
|
+
*
|
|
135
|
+
* Only splits on dots followed by a letter so that version numbers
|
|
136
|
+
* embedded in operationIds (e.g. `v2025.0`) are kept intact.
|
|
137
|
+
*/
|
|
138
|
+
function applyToFileParts(text, transformPart) {
|
|
139
|
+
const parts = text.split(/\.(?=[a-zA-Z])/);
|
|
140
|
+
return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Converts `text` to camelCase.
|
|
144
|
+
* When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* camelCase('hello-world') // 'helloWorld'
|
|
148
|
+
* camelCase('pet.petId', { isFile: true }) // 'pet/petId'
|
|
149
|
+
*/
|
|
150
|
+
function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
151
|
+
if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
|
|
152
|
+
prefix,
|
|
153
|
+
suffix
|
|
154
|
+
} : {}));
|
|
155
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Converts `text` to PascalCase.
|
|
159
|
+
* When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* pascalCase('hello-world') // 'HelloWorld'
|
|
163
|
+
* pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
|
|
164
|
+
*/
|
|
165
|
+
function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
166
|
+
if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
|
|
167
|
+
prefix,
|
|
168
|
+
suffix
|
|
169
|
+
}) : camelCase(part));
|
|
170
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region ../../internals/utils/src/reserved.ts
|
|
174
|
+
/**
|
|
175
|
+
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* isValidVarName('status') // true
|
|
180
|
+
* isValidVarName('class') // false (reserved word)
|
|
181
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
function isValidVarName(name) {
|
|
185
|
+
try {
|
|
186
|
+
new Function(`var ${name}`);
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region ../../internals/utils/src/string.ts
|
|
194
|
+
/**
|
|
195
|
+
* Strips the file extension from a path or file name.
|
|
196
|
+
* Only removes the last `.ext` segment when the dot is not part of a directory name.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* trimExtName('petStore.ts') // 'petStore'
|
|
200
|
+
* trimExtName('/src/models/pet.ts') // '/src/models/pet'
|
|
201
|
+
* trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'
|
|
202
|
+
* trimExtName('noExtension') // 'noExtension'
|
|
203
|
+
*/
|
|
204
|
+
function trimExtName(text) {
|
|
205
|
+
const dotIndex = text.lastIndexOf(".");
|
|
206
|
+
if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
|
|
207
|
+
return text;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/guards.ts
|
|
211
|
+
/**
|
|
212
|
+
* Narrows a `SchemaNode` to the variant that matches `type`.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* const schema = createSchema({ type: 'string' })
|
|
217
|
+
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | undefined
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
function narrowSchema(node, type) {
|
|
221
|
+
return node?.type === type ? node : void 0;
|
|
222
|
+
}
|
|
223
|
+
function isKind(kind) {
|
|
224
|
+
return (node) => node.kind === kind;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Returns `true` when the input is an `InputNode`.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* if (isInputNode(node)) {
|
|
232
|
+
* console.log(node.schemas.length)
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
const isInputNode = isKind("Input");
|
|
237
|
+
/**
|
|
238
|
+
* Returns `true` when the input is an `OutputNode`.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* if (isOutputNode(node)) {
|
|
243
|
+
* console.log(node.files.length)
|
|
244
|
+
* }
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
const isOutputNode = isKind("Output");
|
|
248
|
+
/**
|
|
249
|
+
* Returns `true` when the input is an `OperationNode`.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* if (isOperationNode(node)) {
|
|
254
|
+
* console.log(node.operationId)
|
|
255
|
+
* }
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
const isOperationNode = isKind("Operation");
|
|
259
|
+
/**
|
|
260
|
+
* Returns `true` when the input is a `SchemaNode`.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* if (isSchemaNode(node)) {
|
|
265
|
+
* console.log(node.type)
|
|
266
|
+
* }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
const isSchemaNode = isKind("Schema");
|
|
270
|
+
isKind("Property");
|
|
271
|
+
isKind("Parameter");
|
|
272
|
+
isKind("Response");
|
|
273
|
+
isKind("FunctionParameter");
|
|
274
|
+
isKind("ParameterGroup");
|
|
275
|
+
isKind("FunctionParameters");
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/utils.ts
|
|
278
|
+
const plainStringTypes = new Set([
|
|
279
|
+
"string",
|
|
280
|
+
"uuid",
|
|
281
|
+
"email",
|
|
282
|
+
"url",
|
|
283
|
+
"datetime"
|
|
284
|
+
]);
|
|
285
|
+
/**
|
|
286
|
+
* Returns a merged schema view for a ref node, combining the resolved `node.schema`
|
|
287
|
+
* (base from the referenced definition) with any usage-site sibling fields set directly
|
|
288
|
+
* on the ref node (description, readOnly, nullable, deprecated, etc.).
|
|
289
|
+
*
|
|
290
|
+
* Usage-site fields take precedence over the resolved schema's own fields when both are defined.
|
|
291
|
+
*
|
|
292
|
+
* For non-ref nodes the node itself is returned unchanged.
|
|
293
|
+
*/
|
|
294
|
+
function syncSchemaRef(node) {
|
|
295
|
+
const ref = narrowSchema(node, "ref");
|
|
296
|
+
if (!ref) return node;
|
|
297
|
+
if (!ref.schema) return node;
|
|
298
|
+
const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref;
|
|
299
|
+
const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
|
|
300
|
+
return createSchema({
|
|
301
|
+
...ref.schema,
|
|
302
|
+
...definedOverrides
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Returns `true` when a schema is emitted as a plain `string` type.
|
|
307
|
+
*
|
|
308
|
+
* - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings.
|
|
309
|
+
* - `date` and `time` are plain strings when their `representation` is `'string'` rather than `'date'`.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* isStringType(createSchema({ type: 'uuid' })) // true
|
|
314
|
+
* isStringType(createSchema({ type: 'date', representation: 'date' })) // false
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
function isStringType(node) {
|
|
318
|
+
if (plainStringTypes.has(node.type)) return true;
|
|
319
|
+
const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
|
|
320
|
+
if (temporal) return temporal.representation !== "date";
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Applies casing rules to parameter names and returns a new parameter array.
|
|
325
|
+
*
|
|
326
|
+
* The input array is not mutated.
|
|
327
|
+
* If `casing` is not set, the original array is returned unchanged.
|
|
328
|
+
*
|
|
329
|
+
* Use this before passing parameters to schema builders so that property keys
|
|
330
|
+
* in generated output match the desired casing while preserving
|
|
331
|
+
* `OperationNode.parameters` for other consumers.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* const params = [createParameter({ name: 'pet_id', in: 'query', schema: createSchema({ type: 'string' }) })]
|
|
336
|
+
* const cased = caseParams(params, 'camelcase')
|
|
337
|
+
* // cased[0].name === 'petId'
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
function caseParams(params, casing) {
|
|
341
|
+
if (!casing) return params;
|
|
342
|
+
return params.map((param) => {
|
|
343
|
+
const transformed = casing === "camelcase" || !isValidVarName(param.name) ? camelCase(param.name) : param.name;
|
|
344
|
+
return {
|
|
345
|
+
...param,
|
|
346
|
+
name: transformed
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
356
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
360
|
+
return createSchema({
|
|
361
|
+
type: "object",
|
|
362
|
+
primitive: "object",
|
|
363
|
+
properties: [createProperty({
|
|
364
|
+
name: propertyName,
|
|
365
|
+
schema: createSchema({
|
|
366
|
+
type: "enum",
|
|
367
|
+
primitive: "string",
|
|
368
|
+
enumValues: [value]
|
|
369
|
+
}),
|
|
370
|
+
required: true
|
|
371
|
+
})]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function resolveParamsType({ node, param, resolver }) {
|
|
375
|
+
if (!resolver) return createParamsType({
|
|
376
|
+
variant: "reference",
|
|
377
|
+
name: param.schema.primitive ?? "unknown"
|
|
378
|
+
});
|
|
379
|
+
const individualName = resolver.resolveParamName(node, param);
|
|
380
|
+
const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
|
|
381
|
+
const groupResolvers = {
|
|
382
|
+
path: resolver.resolvePathParamsName,
|
|
383
|
+
query: resolver.resolveQueryParamsName,
|
|
384
|
+
header: resolver.resolveHeaderParamsName
|
|
385
|
+
};
|
|
386
|
+
const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
|
|
387
|
+
if (groupName && groupName !== individualName) return createParamsType({
|
|
388
|
+
variant: "member",
|
|
389
|
+
base: groupName,
|
|
390
|
+
key: param.name
|
|
391
|
+
});
|
|
392
|
+
return createParamsType({
|
|
393
|
+
variant: "reference",
|
|
394
|
+
name: individualName
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Converts an {@link OperationNode} into a {@link FunctionParametersNode}.
|
|
399
|
+
*
|
|
400
|
+
* Centralizes the per-plugin `getParams()` pattern. Provide a `resolver` for
|
|
401
|
+
* type resolution and `extraParams` for plugin-specific trailing parameters.
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```ts
|
|
405
|
+
* const params = createOperationParams(node, {
|
|
406
|
+
* paramsType: 'inline',
|
|
407
|
+
* pathParamsType: 'inline',
|
|
408
|
+
* resolver: tsResolver,
|
|
409
|
+
* extraParams: [createFunctionParameter({ name: 'options', type: createParamsType({ variant: 'reference', name: 'Partial<RequestOptions>' }), default: '{}' })],
|
|
410
|
+
* })
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
function createOperationParams(node, options) {
|
|
414
|
+
const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
|
|
415
|
+
const dataName = paramNames?.data ?? "data";
|
|
416
|
+
const paramsName = paramNames?.params ?? "params";
|
|
417
|
+
const headersName = paramNames?.headers ?? "headers";
|
|
418
|
+
const pathName = paramNames?.path ?? "pathParams";
|
|
419
|
+
const wrapType = (type) => createParamsType({
|
|
420
|
+
variant: "reference",
|
|
421
|
+
name: typeWrapper ? typeWrapper(type) : type
|
|
422
|
+
});
|
|
423
|
+
const wrapTypeNode = (type) => type.kind === "ParamsType" && type.variant === "reference" ? wrapType(type.name) : type;
|
|
424
|
+
const casedParams = caseParams(node.parameters, paramsCasing);
|
|
425
|
+
const pathParams = casedParams.filter((p) => p.in === "path");
|
|
426
|
+
const queryParams = casedParams.filter((p) => p.in === "query");
|
|
427
|
+
const headerParams = casedParams.filter((p) => p.in === "header");
|
|
428
|
+
const bodyType = node.requestBody?.schema ? wrapType(resolver?.resolveDataName(node) ?? "unknown") : void 0;
|
|
429
|
+
const bodyRequired = node.requestBody?.required ?? false;
|
|
430
|
+
const queryGroupType = resolver ? resolveGroupType({
|
|
431
|
+
node,
|
|
432
|
+
params: queryParams,
|
|
433
|
+
groupMethod: resolver.resolveQueryParamsName,
|
|
434
|
+
resolver
|
|
435
|
+
}) : void 0;
|
|
436
|
+
const headerGroupType = resolver ? resolveGroupType({
|
|
437
|
+
node,
|
|
438
|
+
params: headerParams,
|
|
439
|
+
groupMethod: resolver.resolveHeaderParamsName,
|
|
440
|
+
resolver
|
|
441
|
+
}) : void 0;
|
|
442
|
+
const params = [];
|
|
443
|
+
if (paramsType === "object") {
|
|
444
|
+
const children = [
|
|
445
|
+
...pathParams.map((p) => {
|
|
446
|
+
const type = resolveParamsType({
|
|
447
|
+
node,
|
|
448
|
+
param: p,
|
|
449
|
+
resolver
|
|
450
|
+
});
|
|
451
|
+
return createFunctionParameter({
|
|
452
|
+
name: p.name,
|
|
453
|
+
type: wrapTypeNode(type),
|
|
454
|
+
optional: !p.required
|
|
455
|
+
});
|
|
456
|
+
}),
|
|
457
|
+
...bodyType ? [createFunctionParameter({
|
|
458
|
+
name: dataName,
|
|
459
|
+
type: bodyType,
|
|
460
|
+
optional: !bodyRequired
|
|
461
|
+
})] : [],
|
|
462
|
+
...buildGroupParam({
|
|
463
|
+
name: paramsName,
|
|
464
|
+
node,
|
|
465
|
+
params: queryParams,
|
|
466
|
+
groupType: queryGroupType,
|
|
467
|
+
resolver,
|
|
468
|
+
wrapType
|
|
469
|
+
}),
|
|
470
|
+
...buildGroupParam({
|
|
471
|
+
name: headersName,
|
|
472
|
+
node,
|
|
473
|
+
params: headerParams,
|
|
474
|
+
groupType: headerGroupType,
|
|
475
|
+
resolver,
|
|
476
|
+
wrapType
|
|
477
|
+
})
|
|
478
|
+
];
|
|
479
|
+
if (children.length) params.push(createParameterGroup({
|
|
480
|
+
properties: children,
|
|
481
|
+
default: children.every((c) => c.optional) ? "{}" : void 0
|
|
482
|
+
}));
|
|
483
|
+
} else {
|
|
484
|
+
if (pathParams.length) if (pathParamsType === "inlineSpread") {
|
|
485
|
+
const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]) ?? void 0;
|
|
486
|
+
params.push(createFunctionParameter({
|
|
487
|
+
name: pathName,
|
|
488
|
+
type: spreadType ? wrapType(spreadType) : void 0,
|
|
489
|
+
rest: true
|
|
490
|
+
}));
|
|
491
|
+
} else {
|
|
492
|
+
const pathChildren = pathParams.map((p) => {
|
|
493
|
+
const type = resolveParamsType({
|
|
494
|
+
node,
|
|
495
|
+
param: p,
|
|
496
|
+
resolver
|
|
497
|
+
});
|
|
498
|
+
return createFunctionParameter({
|
|
499
|
+
name: p.name,
|
|
500
|
+
type: wrapTypeNode(type),
|
|
501
|
+
optional: !p.required
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
params.push(createParameterGroup({
|
|
505
|
+
properties: pathChildren,
|
|
506
|
+
inline: pathParamsType === "inline",
|
|
507
|
+
default: pathParamsDefault ?? (pathChildren.every((c) => c.optional) ? "{}" : void 0)
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
if (bodyType) params.push(createFunctionParameter({
|
|
511
|
+
name: dataName,
|
|
512
|
+
type: bodyType,
|
|
513
|
+
optional: !bodyRequired
|
|
514
|
+
}));
|
|
515
|
+
params.push(...buildGroupParam({
|
|
516
|
+
name: paramsName,
|
|
517
|
+
node,
|
|
518
|
+
params: queryParams,
|
|
519
|
+
groupType: queryGroupType,
|
|
520
|
+
resolver,
|
|
521
|
+
wrapType
|
|
522
|
+
}));
|
|
523
|
+
params.push(...buildGroupParam({
|
|
524
|
+
name: headersName,
|
|
525
|
+
node,
|
|
526
|
+
params: headerParams,
|
|
527
|
+
groupType: headerGroupType,
|
|
528
|
+
resolver,
|
|
529
|
+
wrapType
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
params.push(...extraParams);
|
|
533
|
+
return createFunctionParameters({ params });
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Builds a single {@link FunctionParameterNode} for a query or header group.
|
|
537
|
+
* Returns an empty array when there are no params to emit.
|
|
538
|
+
*
|
|
539
|
+
* If a pre-resolved `groupType` is provided it emits `name: GroupType`.
|
|
540
|
+
* Otherwise, it builds an inline struct from the individual params.
|
|
541
|
+
*/
|
|
542
|
+
function buildGroupParam({ name, node, params, groupType, resolver, wrapType }) {
|
|
543
|
+
if (groupType) return [createFunctionParameter({
|
|
544
|
+
name,
|
|
545
|
+
type: groupType.type.kind === "ParamsType" && groupType.type.variant === "reference" ? wrapType(groupType.type.name) : groupType.type,
|
|
546
|
+
optional: groupType.optional
|
|
547
|
+
})];
|
|
548
|
+
if (params.length) return [createFunctionParameter({
|
|
549
|
+
name,
|
|
550
|
+
type: toStructType({
|
|
551
|
+
node,
|
|
552
|
+
params,
|
|
553
|
+
resolver
|
|
554
|
+
}),
|
|
555
|
+
optional: params.every((p) => !p.required)
|
|
556
|
+
})];
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Derives a {@link ParamGroupType} from the resolver's group method.
|
|
561
|
+
* Returns `undefined` when the group name equals the individual param name (no real group).
|
|
562
|
+
*/
|
|
563
|
+
function resolveGroupType({ node, params, groupMethod, resolver }) {
|
|
564
|
+
if (!params.length) return;
|
|
565
|
+
const firstParam = params[0];
|
|
566
|
+
const groupName = groupMethod.call(resolver, node, firstParam);
|
|
567
|
+
if (groupName === resolver.resolveParamName(node, firstParam)) return;
|
|
568
|
+
const allOptional = params.every((p) => !p.required);
|
|
569
|
+
return {
|
|
570
|
+
type: createParamsType({
|
|
571
|
+
variant: "reference",
|
|
572
|
+
name: groupName
|
|
573
|
+
}),
|
|
574
|
+
optional: allOptional
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Builds a {@link TypeNode} with `variant: 'struct'` for an inline anonymous type grouping named fields.
|
|
579
|
+
*
|
|
580
|
+
* Used when query or header parameters have no dedicated group type name.
|
|
581
|
+
* Each language printer renders this appropriately (TypeScript: `{ petId: string; name?: string }`).
|
|
582
|
+
*/
|
|
583
|
+
function toStructType({ node, params, resolver }) {
|
|
584
|
+
return createParamsType({
|
|
585
|
+
variant: "struct",
|
|
586
|
+
properties: params.map((p) => ({
|
|
587
|
+
name: p.name,
|
|
588
|
+
optional: !p.required,
|
|
589
|
+
type: resolveParamsType({
|
|
590
|
+
node,
|
|
591
|
+
param: p,
|
|
592
|
+
resolver
|
|
593
|
+
})
|
|
594
|
+
}))
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
function sourceKey(source) {
|
|
598
|
+
return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
|
|
599
|
+
}
|
|
600
|
+
function pathTypeKey(path, isTypeOnly) {
|
|
601
|
+
return `${path}:${isTypeOnly ?? false}`;
|
|
602
|
+
}
|
|
603
|
+
function exportKey(path, name, isTypeOnly, asAlias) {
|
|
604
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
605
|
+
}
|
|
606
|
+
function importKey(path, name, isTypeOnly) {
|
|
607
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Computes a multi-level sort key for exports and imports:
|
|
611
|
+
* non-array names first (wildcards/namespace aliases); type-only before value; alphabetical path; unnamed before named.
|
|
612
|
+
*/
|
|
613
|
+
function sortKey(node) {
|
|
614
|
+
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
615
|
+
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
616
|
+
const hasName = node.name != null ? "1" : "0";
|
|
617
|
+
const name = Array.isArray(node.name) ? [...node.name].sort().join("\0") : node.name ?? "";
|
|
618
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Deduplicates an array of `SourceNode` objects.
|
|
622
|
+
* Named sources are deduplicated by `name + isExportable + isTypeOnly`.
|
|
623
|
+
* Unnamed sources are deduplicated by object reference.
|
|
624
|
+
*/
|
|
625
|
+
function combineSources(sources) {
|
|
626
|
+
const seen = /* @__PURE__ */ new Map();
|
|
627
|
+
for (const source of sources) {
|
|
628
|
+
const key = sourceKey(source);
|
|
629
|
+
if (!seen.has(key)) seen.set(key, source);
|
|
630
|
+
}
|
|
631
|
+
return [...seen.values()];
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Deduplicates and merges an array of `ExportNode` objects.
|
|
635
|
+
* Exports with the same path and `isTypeOnly` flag have their names merged.
|
|
636
|
+
*/
|
|
637
|
+
function combineExports(exports) {
|
|
638
|
+
const result = [];
|
|
639
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
640
|
+
const seen = /* @__PURE__ */ new Set();
|
|
641
|
+
for (const curr of [...exports].sort((a, b) => {
|
|
642
|
+
const ka = sortKey(a);
|
|
643
|
+
const kb = sortKey(b);
|
|
644
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
645
|
+
})) {
|
|
646
|
+
const { name, path, isTypeOnly, asAlias } = curr;
|
|
647
|
+
if (Array.isArray(name)) {
|
|
648
|
+
if (!name.length) continue;
|
|
649
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
650
|
+
const existing = namedByPath.get(key);
|
|
651
|
+
if (existing && Array.isArray(existing.name)) existing.name = [...new Set([...existing.name, ...name])];
|
|
652
|
+
else {
|
|
653
|
+
const newItem = {
|
|
654
|
+
...curr,
|
|
655
|
+
name: [...new Set(name)]
|
|
656
|
+
};
|
|
657
|
+
result.push(newItem);
|
|
658
|
+
namedByPath.set(key, newItem);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
const key = exportKey(path, name, isTypeOnly, asAlias);
|
|
662
|
+
if (!seen.has(key)) {
|
|
663
|
+
result.push(curr);
|
|
664
|
+
seen.add(key);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Deduplicates and merges an array of `ImportNode` objects.
|
|
672
|
+
* Filters out unused imports (names not referenced in `source` or re-exported).
|
|
673
|
+
* Imports with the same path and `isTypeOnly` flag have their names merged.
|
|
674
|
+
*/
|
|
675
|
+
function combineImports(imports, exports, source) {
|
|
676
|
+
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
677
|
+
const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
|
|
678
|
+
const result = [];
|
|
679
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
680
|
+
const seen = /* @__PURE__ */ new Set();
|
|
681
|
+
for (const curr of [...imports].sort((a, b) => {
|
|
682
|
+
const ka = sortKey(a);
|
|
683
|
+
const kb = sortKey(b);
|
|
684
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
685
|
+
})) {
|
|
686
|
+
if (curr.path === curr.root) continue;
|
|
687
|
+
const { path, isTypeOnly } = curr;
|
|
688
|
+
let { name } = curr;
|
|
689
|
+
if (Array.isArray(name)) {
|
|
690
|
+
name = [...new Set(name)].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.propertyName));
|
|
691
|
+
if (!name.length) continue;
|
|
692
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
693
|
+
const existing = namedByPath.get(key);
|
|
694
|
+
if (existing && Array.isArray(existing.name)) existing.name = [...new Set([...existing.name, ...name])];
|
|
695
|
+
else {
|
|
696
|
+
const newItem = {
|
|
697
|
+
...curr,
|
|
698
|
+
name
|
|
699
|
+
};
|
|
700
|
+
result.push(newItem);
|
|
701
|
+
namedByPath.set(key, newItem);
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
if (name && !isUsed(name)) continue;
|
|
705
|
+
const key = importKey(path, name, isTypeOnly);
|
|
706
|
+
if (!seen.has(key)) {
|
|
707
|
+
result.push(curr);
|
|
708
|
+
seen.add(key);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return result;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Recursively extracts all string content embedded in a {@link CodeNode} tree.
|
|
716
|
+
*
|
|
717
|
+
* Includes text node values, and string attribute fields (`params`, `generics`,
|
|
718
|
+
* `returnType`, `type`) that may reference identifiers needing imports.
|
|
719
|
+
* Used by `createFile` to build the full source string for import filtering.
|
|
720
|
+
*/
|
|
721
|
+
function extractStringsFromNodes(nodes) {
|
|
722
|
+
if (!nodes?.length) return "";
|
|
723
|
+
return nodes.map((node) => {
|
|
724
|
+
if (typeof node === "string") return node;
|
|
725
|
+
if (node.kind === "Text") return node.value;
|
|
726
|
+
if (node.kind === "Break") return "";
|
|
727
|
+
if (node.kind === "Jsx") return node.value;
|
|
728
|
+
const parts = [];
|
|
729
|
+
if ("params" in node && node.params) parts.push(node.params);
|
|
730
|
+
if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
|
|
731
|
+
if ("returnType" in node && node.returnType) parts.push(node.returnType);
|
|
732
|
+
if ("type" in node && typeof node.type === "string") parts.push(node.type);
|
|
733
|
+
const nested = extractStringsFromNodes(node.nodes);
|
|
734
|
+
if (nested) parts.push(nested);
|
|
735
|
+
return parts.join("\n");
|
|
736
|
+
}).filter(Boolean).join("\n");
|
|
737
|
+
}
|
|
738
|
+
//#endregion
|
|
72
739
|
//#region src/factory.ts
|
|
73
740
|
/**
|
|
74
|
-
*
|
|
741
|
+
* Syncs property/parameter schema optionality flags from `required` and `schema.nullable`.
|
|
742
|
+
*
|
|
743
|
+
* - `optional` is set for non-required, non-nullable schemas.
|
|
744
|
+
* - `nullish` is set for non-required, nullable schemas.
|
|
745
|
+
*/
|
|
746
|
+
function syncOptionality(schema, required) {
|
|
747
|
+
const nullable = schema.nullable ?? false;
|
|
748
|
+
return {
|
|
749
|
+
...schema,
|
|
750
|
+
optional: !required && !nullable ? true : void 0,
|
|
751
|
+
nullish: !required && nullable ? true : void 0
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Creates an `InputNode` with stable defaults for `schemas` and `operations`.
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* ```ts
|
|
759
|
+
* const input = createInput()
|
|
760
|
+
* // { kind: 'Input', schemas: [], operations: [] }
|
|
761
|
+
* ```
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* ```ts
|
|
765
|
+
* const input = createInput({ schemas: [petSchema] })
|
|
766
|
+
* // keeps default operations: []
|
|
767
|
+
* ```
|
|
75
768
|
*/
|
|
76
|
-
function
|
|
769
|
+
function createInput(overrides = {}) {
|
|
77
770
|
return {
|
|
78
771
|
schemas: [],
|
|
79
772
|
operations: [],
|
|
80
773
|
...overrides,
|
|
81
|
-
kind: "
|
|
774
|
+
kind: "Input"
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Creates an `OutputNode` with a stable default for `files`.
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* ```ts
|
|
782
|
+
* const output = createOutput()
|
|
783
|
+
* // { kind: 'Output', files: [] }
|
|
784
|
+
* ```
|
|
785
|
+
*
|
|
786
|
+
* @example
|
|
787
|
+
* ```ts
|
|
788
|
+
* const output = createOutput({ files: [petFile] })
|
|
789
|
+
* ```
|
|
790
|
+
*/
|
|
791
|
+
function createOutput(overrides = {}) {
|
|
792
|
+
return {
|
|
793
|
+
files: [],
|
|
794
|
+
...overrides,
|
|
795
|
+
kind: "Output"
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Creates an `OperationNode` with default empty arrays for `tags`, `parameters`, and `responses`.
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* ```ts
|
|
803
|
+
* const operation = createOperation({
|
|
804
|
+
* operationId: 'getPetById',
|
|
805
|
+
* method: 'GET',
|
|
806
|
+
* path: '/pet/{petId}',
|
|
807
|
+
* })
|
|
808
|
+
* // tags, parameters, and responses are []
|
|
809
|
+
* ```
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```ts
|
|
813
|
+
* const operation = createOperation({
|
|
814
|
+
* operationId: 'findPets',
|
|
815
|
+
* method: 'GET',
|
|
816
|
+
* path: '/pet/findByStatus',
|
|
817
|
+
* tags: ['pet'],
|
|
818
|
+
* })
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
function createOperation(props) {
|
|
822
|
+
return {
|
|
823
|
+
tags: [],
|
|
824
|
+
parameters: [],
|
|
825
|
+
responses: [],
|
|
826
|
+
...props,
|
|
827
|
+
kind: "Operation"
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Maps schema `type` to its underlying `primitive`.
|
|
832
|
+
* Primitive types map to themselves; special string formats map to `'string'`.
|
|
833
|
+
* Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
|
|
834
|
+
*/
|
|
835
|
+
const TYPE_TO_PRIMITIVE = {
|
|
836
|
+
string: "string",
|
|
837
|
+
number: "number",
|
|
838
|
+
integer: "integer",
|
|
839
|
+
bigint: "bigint",
|
|
840
|
+
boolean: "boolean",
|
|
841
|
+
null: "null",
|
|
842
|
+
any: "any",
|
|
843
|
+
unknown: "unknown",
|
|
844
|
+
void: "void",
|
|
845
|
+
never: "never",
|
|
846
|
+
object: "object",
|
|
847
|
+
array: "array",
|
|
848
|
+
date: "date",
|
|
849
|
+
uuid: "string",
|
|
850
|
+
email: "string",
|
|
851
|
+
url: "string",
|
|
852
|
+
datetime: "string",
|
|
853
|
+
time: "string"
|
|
854
|
+
};
|
|
855
|
+
function createSchema(props) {
|
|
856
|
+
const inferredPrimitive = TYPE_TO_PRIMITIVE[props.type];
|
|
857
|
+
if (props["type"] === "object") return {
|
|
858
|
+
properties: [],
|
|
859
|
+
primitive: "object",
|
|
860
|
+
...props,
|
|
861
|
+
kind: "Schema"
|
|
862
|
+
};
|
|
863
|
+
return {
|
|
864
|
+
primitive: inferredPrimitive,
|
|
865
|
+
...props,
|
|
866
|
+
kind: "Schema"
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Creates a `PropertyNode`.
|
|
871
|
+
*
|
|
872
|
+
* `required` defaults to `false`.
|
|
873
|
+
* `schema.optional` and `schema.nullish` are derived from `required` and `schema.nullable`.
|
|
874
|
+
*
|
|
875
|
+
* @example
|
|
876
|
+
* ```ts
|
|
877
|
+
* const property = createProperty({
|
|
878
|
+
* name: 'status',
|
|
879
|
+
* schema: createSchema({ type: 'string' }),
|
|
880
|
+
* })
|
|
881
|
+
* // required=false, schema.optional=true
|
|
882
|
+
* ```
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* ```ts
|
|
886
|
+
* const property = createProperty({
|
|
887
|
+
* name: 'status',
|
|
888
|
+
* required: true,
|
|
889
|
+
* schema: createSchema({ type: 'string', nullable: true }),
|
|
890
|
+
* })
|
|
891
|
+
* // required=true, no optional/nullish
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
function createProperty(props) {
|
|
895
|
+
const required = props.required ?? false;
|
|
896
|
+
return {
|
|
897
|
+
...props,
|
|
898
|
+
kind: "Property",
|
|
899
|
+
required,
|
|
900
|
+
schema: syncOptionality(props.schema, required)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Creates a `ParameterNode`.
|
|
905
|
+
*
|
|
906
|
+
* `required` defaults to `false`.
|
|
907
|
+
* Nested schema flags are set from `required` and `schema.nullable`.
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* ```ts
|
|
911
|
+
* const param = createParameter({
|
|
912
|
+
* name: 'petId',
|
|
913
|
+
* in: 'path',
|
|
914
|
+
* required: true,
|
|
915
|
+
* schema: createSchema({ type: 'string' }),
|
|
916
|
+
* })
|
|
917
|
+
* ```
|
|
918
|
+
*
|
|
919
|
+
* @example
|
|
920
|
+
* ```ts
|
|
921
|
+
* const param = createParameter({
|
|
922
|
+
* name: 'status',
|
|
923
|
+
* in: 'query',
|
|
924
|
+
* schema: createSchema({ type: 'string', nullable: true }),
|
|
925
|
+
* })
|
|
926
|
+
* // required=false, schema.nullish=true
|
|
927
|
+
* ```
|
|
928
|
+
*/
|
|
929
|
+
function createParameter(props) {
|
|
930
|
+
const required = props.required ?? false;
|
|
931
|
+
return {
|
|
932
|
+
...props,
|
|
933
|
+
kind: "Parameter",
|
|
934
|
+
required,
|
|
935
|
+
schema: syncOptionality(props.schema, required)
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Creates a `ResponseNode`.
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```ts
|
|
943
|
+
* const response = createResponse({
|
|
944
|
+
* statusCode: '200',
|
|
945
|
+
* description: 'Success',
|
|
946
|
+
* schema: createSchema({ type: 'object', properties: [] }),
|
|
947
|
+
* })
|
|
948
|
+
* ```
|
|
949
|
+
*/
|
|
950
|
+
function createResponse(props) {
|
|
951
|
+
return {
|
|
952
|
+
...props,
|
|
953
|
+
kind: "Response"
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Creates a `FunctionParameterNode`.
|
|
958
|
+
*
|
|
959
|
+
* `optional` defaults to `false`.
|
|
960
|
+
*
|
|
961
|
+
* @example Required typed param
|
|
962
|
+
* ```ts
|
|
963
|
+
* createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }) })
|
|
964
|
+
* // → petId: string
|
|
965
|
+
* ```
|
|
966
|
+
*
|
|
967
|
+
* @example Optional param
|
|
968
|
+
* ```ts
|
|
969
|
+
* createFunctionParameter({ name: 'params', type: createParamsType({ variant: 'reference', name: 'QueryParams' }), optional: true })
|
|
970
|
+
* // → params?: QueryParams
|
|
971
|
+
* ```
|
|
972
|
+
*
|
|
973
|
+
* @example Param with default (implicitly optional; cannot combine with `optional: true`)
|
|
974
|
+
* ```ts
|
|
975
|
+
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
|
|
976
|
+
* // → config: RequestConfig = {}
|
|
977
|
+
* ```
|
|
978
|
+
*/
|
|
979
|
+
function createFunctionParameter(props) {
|
|
980
|
+
return {
|
|
981
|
+
optional: false,
|
|
982
|
+
...props,
|
|
983
|
+
kind: "FunctionParameter"
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Creates a {@link TypeNode} representing a language-agnostic structured type expression.
|
|
988
|
+
*
|
|
989
|
+
* Use `variant: 'struct'` for inline anonymous types and `variant: 'member'` for a single
|
|
990
|
+
* named field accessed from a group type. Each language's printer renders the variant
|
|
991
|
+
* into its own syntax (TypeScript, Python, C#, Kotlin, …).
|
|
992
|
+
*
|
|
993
|
+
* @example Reference type (TypeScript: `QueryParams`)
|
|
994
|
+
* ```ts
|
|
995
|
+
* createParamsType({ variant: 'reference', name: 'QueryParams' })
|
|
996
|
+
* ```
|
|
997
|
+
*
|
|
998
|
+
* @example Struct type (TypeScript: `{ petId: string }`)
|
|
999
|
+
* ```ts
|
|
1000
|
+
* createParamsType({ variant: 'struct', properties: [{ name: 'petId', optional: false, type: createParamsType({ variant: 'reference', name: 'string' }) }] })
|
|
1001
|
+
* ```
|
|
1002
|
+
*
|
|
1003
|
+
* @example Member type (TypeScript: `DeletePetPathParams['petId']`)
|
|
1004
|
+
* ```ts
|
|
1005
|
+
* createParamsType({ variant: 'member', base: 'DeletePetPathParams', key: 'petId' })
|
|
1006
|
+
* ```
|
|
1007
|
+
*/
|
|
1008
|
+
function createParamsType(props) {
|
|
1009
|
+
return {
|
|
1010
|
+
...props,
|
|
1011
|
+
kind: "ParamsType"
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Creates a `ParameterGroupNode` representing a group of related parameters treated as a unit.
|
|
1016
|
+
*
|
|
1017
|
+
* @example Grouped param (TypeScript declaration)
|
|
1018
|
+
* ```ts
|
|
1019
|
+
* createParameterGroup({
|
|
1020
|
+
* properties: [
|
|
1021
|
+
* createFunctionParameter({ name: 'id', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
|
|
1022
|
+
* createFunctionParameter({ name: 'name', type: createParamsType({ variant: 'reference', name: 'string' }), optional: true }),
|
|
1023
|
+
* ],
|
|
1024
|
+
* default: '{}',
|
|
1025
|
+
* })
|
|
1026
|
+
* // declaration → { id, name? }: { id: string; name?: string } = {}
|
|
1027
|
+
* // call → { id, name }
|
|
1028
|
+
* ```
|
|
1029
|
+
*
|
|
1030
|
+
* @example Inline (spread) — children emitted as individual top-level parameters
|
|
1031
|
+
* ```ts
|
|
1032
|
+
* createParameterGroup({
|
|
1033
|
+
* properties: [createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false })],
|
|
1034
|
+
* inline: true,
|
|
1035
|
+
* })
|
|
1036
|
+
* // declaration → petId: string
|
|
1037
|
+
* // call → petId
|
|
1038
|
+
* ```
|
|
1039
|
+
*/
|
|
1040
|
+
function createParameterGroup(props) {
|
|
1041
|
+
return {
|
|
1042
|
+
...props,
|
|
1043
|
+
kind: "ParameterGroup"
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Creates a `FunctionParametersNode` from an ordered list of parameters.
|
|
1048
|
+
*
|
|
1049
|
+
* @example
|
|
1050
|
+
* ```ts
|
|
1051
|
+
* createFunctionParameters({
|
|
1052
|
+
* params: [
|
|
1053
|
+
* createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
|
|
1054
|
+
* createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), optional: false, default: '{}' }),
|
|
1055
|
+
* ],
|
|
1056
|
+
* })
|
|
1057
|
+
* ```
|
|
1058
|
+
*
|
|
1059
|
+
* @example
|
|
1060
|
+
* ```ts
|
|
1061
|
+
* const empty = createFunctionParameters()
|
|
1062
|
+
* // { kind: 'FunctionParameters', params: [] }
|
|
1063
|
+
* ```
|
|
1064
|
+
*/
|
|
1065
|
+
function createFunctionParameters(props = {}) {
|
|
1066
|
+
return {
|
|
1067
|
+
params: [],
|
|
1068
|
+
...props,
|
|
1069
|
+
kind: "FunctionParameters"
|
|
82
1070
|
};
|
|
83
1071
|
}
|
|
84
1072
|
/**
|
|
85
|
-
* Creates an `
|
|
1073
|
+
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
1074
|
+
*
|
|
1075
|
+
* @example Named import
|
|
1076
|
+
* ```ts
|
|
1077
|
+
* createImport({ name: ['useState'], path: 'react' })
|
|
1078
|
+
* // import { useState } from 'react'
|
|
1079
|
+
* ```
|
|
1080
|
+
*
|
|
1081
|
+
* @example Type-only import
|
|
1082
|
+
* ```ts
|
|
1083
|
+
* createImport({ name: ['FC'], path: 'react', isTypeOnly: true })
|
|
1084
|
+
* // import type { FC } from 'react'
|
|
1085
|
+
* ```
|
|
86
1086
|
*/
|
|
87
|
-
function
|
|
1087
|
+
function createImport(props) {
|
|
88
1088
|
return {
|
|
89
|
-
tags: [],
|
|
90
|
-
parameters: [],
|
|
91
|
-
responses: [],
|
|
92
1089
|
...props,
|
|
93
|
-
kind: "
|
|
1090
|
+
kind: "Import"
|
|
94
1091
|
};
|
|
95
1092
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1093
|
+
/**
|
|
1094
|
+
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
1095
|
+
*
|
|
1096
|
+
* @example Named export
|
|
1097
|
+
* ```ts
|
|
1098
|
+
* createExport({ name: ['Pet'], path: './Pet' })
|
|
1099
|
+
* // export { Pet } from './Pet'
|
|
1100
|
+
* ```
|
|
1101
|
+
*
|
|
1102
|
+
* @example Wildcard export
|
|
1103
|
+
* ```ts
|
|
1104
|
+
* createExport({ path: './utils' })
|
|
1105
|
+
* // export * from './utils'
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
function createExport(props) {
|
|
102
1109
|
return {
|
|
103
1110
|
...props,
|
|
104
|
-
kind: "
|
|
1111
|
+
kind: "Export"
|
|
105
1112
|
};
|
|
106
1113
|
}
|
|
107
1114
|
/**
|
|
108
|
-
* Creates a `
|
|
1115
|
+
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
1116
|
+
*
|
|
1117
|
+
* @example
|
|
1118
|
+
* ```ts
|
|
1119
|
+
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
1120
|
+
* ```
|
|
109
1121
|
*/
|
|
110
|
-
function
|
|
1122
|
+
function createSource(props) {
|
|
111
1123
|
return {
|
|
112
|
-
required: false,
|
|
113
1124
|
...props,
|
|
114
|
-
kind: "
|
|
1125
|
+
kind: "Source"
|
|
115
1126
|
};
|
|
116
1127
|
}
|
|
117
1128
|
/**
|
|
118
|
-
* Creates a
|
|
1129
|
+
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1130
|
+
*
|
|
1131
|
+
* Computes:
|
|
1132
|
+
* - `id` — SHA256 hash of the file path
|
|
1133
|
+
* - `name` — `baseName` without extension
|
|
1134
|
+
* - `extname` — extension extracted from `baseName`
|
|
1135
|
+
*
|
|
1136
|
+
* Deduplicates:
|
|
1137
|
+
* - `sources` via `combineSources`
|
|
1138
|
+
* - `exports` via `combineExports`
|
|
1139
|
+
* - `imports` via `combineImports` (also filters unused imports)
|
|
1140
|
+
*
|
|
1141
|
+
* @throws {Error} when `baseName` has no extension.
|
|
1142
|
+
*
|
|
1143
|
+
* @example
|
|
1144
|
+
* ```ts
|
|
1145
|
+
* const file = createFile({
|
|
1146
|
+
* baseName: 'petStore.ts',
|
|
1147
|
+
* path: 'src/models/petStore.ts',
|
|
1148
|
+
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
1149
|
+
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
1150
|
+
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
1151
|
+
* })
|
|
1152
|
+
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
1153
|
+
* // file.name = 'petStore'
|
|
1154
|
+
* // file.extname = '.ts'
|
|
1155
|
+
* ```
|
|
119
1156
|
*/
|
|
120
|
-
function
|
|
1157
|
+
function createFile(input) {
|
|
1158
|
+
const extname = path.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
|
|
1159
|
+
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
1160
|
+
const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
|
|
1161
|
+
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
1162
|
+
const resolvedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
|
|
1163
|
+
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
121
1164
|
return {
|
|
122
|
-
|
|
123
|
-
...
|
|
124
|
-
|
|
1165
|
+
kind: "File",
|
|
1166
|
+
...input,
|
|
1167
|
+
id: createHash("sha256").update(input.path).digest("hex"),
|
|
1168
|
+
name: trimExtName(input.baseName),
|
|
1169
|
+
extname,
|
|
1170
|
+
imports: resolvedImports,
|
|
1171
|
+
exports: resolvedExports,
|
|
1172
|
+
sources: resolvedSources,
|
|
1173
|
+
meta: input.meta ?? {}
|
|
125
1174
|
};
|
|
126
1175
|
}
|
|
127
1176
|
/**
|
|
128
|
-
* Creates a `
|
|
1177
|
+
* Creates a `ConstNode` representing a TypeScript `const` declaration.
|
|
1178
|
+
*
|
|
1179
|
+
* Mirrors the `Const` component from `@kubb/renderer-jsx`.
|
|
1180
|
+
* The component's `children` are represented as `nodes`.
|
|
1181
|
+
*
|
|
1182
|
+
* @example Simple constant
|
|
1183
|
+
* ```ts
|
|
1184
|
+
* createConst({ name: 'pet' })
|
|
1185
|
+
* // const pet = ...
|
|
1186
|
+
* ```
|
|
1187
|
+
*
|
|
1188
|
+
* @example Exported constant with type and `as const`
|
|
1189
|
+
* ```ts
|
|
1190
|
+
* createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
|
|
1191
|
+
* // export const pets: Pet[] = ... as const
|
|
1192
|
+
* ```
|
|
1193
|
+
*
|
|
1194
|
+
* @example With JSDoc and child nodes
|
|
1195
|
+
* ```ts
|
|
1196
|
+
* createConst({
|
|
1197
|
+
* name: 'config',
|
|
1198
|
+
* export: true,
|
|
1199
|
+
* JSDoc: { comments: ['@description App configuration'] },
|
|
1200
|
+
* nodes: [],
|
|
1201
|
+
* })
|
|
1202
|
+
* ```
|
|
129
1203
|
*/
|
|
130
|
-
function
|
|
1204
|
+
function createConst(props) {
|
|
131
1205
|
return {
|
|
132
1206
|
...props,
|
|
133
|
-
kind: "
|
|
1207
|
+
kind: "Const"
|
|
134
1208
|
};
|
|
135
1209
|
}
|
|
136
|
-
//#endregion
|
|
137
|
-
//#region src/guards.ts
|
|
138
1210
|
/**
|
|
139
|
-
*
|
|
1211
|
+
* Creates a `TypeNode` representing a TypeScript `type` alias declaration.
|
|
1212
|
+
*
|
|
1213
|
+
* Mirrors the `Type` component from `@kubb/renderer-jsx`.
|
|
1214
|
+
* The component's `children` are represented as `nodes`.
|
|
1215
|
+
*
|
|
1216
|
+
* @example Simple type alias
|
|
1217
|
+
* ```ts
|
|
1218
|
+
* createType({ name: 'Pet' })
|
|
1219
|
+
* // type Pet = ...
|
|
1220
|
+
* ```
|
|
1221
|
+
*
|
|
1222
|
+
* @example Exported type with JSDoc
|
|
1223
|
+
* ```ts
|
|
1224
|
+
* createType({
|
|
1225
|
+
* name: 'PetStatus',
|
|
1226
|
+
* export: true,
|
|
1227
|
+
* JSDoc: { comments: ['@description Status of a pet'] },
|
|
1228
|
+
* })
|
|
1229
|
+
* // export type PetStatus = ...
|
|
1230
|
+
* ```
|
|
140
1231
|
*/
|
|
141
|
-
function
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1232
|
+
function createType(props) {
|
|
1233
|
+
return {
|
|
1234
|
+
...props,
|
|
1235
|
+
kind: "Type"
|
|
1236
|
+
};
|
|
146
1237
|
}
|
|
147
1238
|
/**
|
|
148
|
-
*
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
1239
|
+
* Creates a `FunctionNode` representing a TypeScript `function` declaration.
|
|
1240
|
+
*
|
|
1241
|
+
* Mirrors the `Function` component from `@kubb/renderer-jsx`.
|
|
1242
|
+
* The component's `children` are represented as `nodes`.
|
|
1243
|
+
*
|
|
1244
|
+
* @example Simple function
|
|
1245
|
+
* ```ts
|
|
1246
|
+
* createFunction({ name: 'getPet' })
|
|
1247
|
+
* // function getPet() { ... }
|
|
1248
|
+
* ```
|
|
1249
|
+
*
|
|
1250
|
+
* @example Exported async function with return type
|
|
1251
|
+
* ```ts
|
|
1252
|
+
* createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
|
|
1253
|
+
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
1254
|
+
* ```
|
|
1255
|
+
*
|
|
1256
|
+
* @example Function with generics and params
|
|
1257
|
+
* ```ts
|
|
1258
|
+
* createFunction({
|
|
1259
|
+
* name: 'identity',
|
|
1260
|
+
* export: true,
|
|
1261
|
+
* generics: ['T'],
|
|
1262
|
+
* params: 'value: T',
|
|
1263
|
+
* returnType: 'T',
|
|
1264
|
+
* })
|
|
1265
|
+
* // export function identity<T>(value: T): T { ... }
|
|
1266
|
+
* ```
|
|
153
1267
|
*/
|
|
154
|
-
|
|
1268
|
+
function createFunction(props) {
|
|
1269
|
+
return {
|
|
1270
|
+
...props,
|
|
1271
|
+
kind: "Function"
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
155
1274
|
/**
|
|
156
|
-
*
|
|
1275
|
+
* Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
|
|
1276
|
+
*
|
|
1277
|
+
* Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
|
|
1278
|
+
* The component's `children` are represented as `nodes`.
|
|
1279
|
+
*
|
|
1280
|
+
* @example Simple arrow function
|
|
1281
|
+
* ```ts
|
|
1282
|
+
* createArrowFunction({ name: 'getPet' })
|
|
1283
|
+
* // const getPet = () => { ... }
|
|
1284
|
+
* ```
|
|
1285
|
+
*
|
|
1286
|
+
* @example Single-line exported arrow function
|
|
1287
|
+
* ```ts
|
|
1288
|
+
* createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
|
|
1289
|
+
* // export const double = (n: number) => ...
|
|
1290
|
+
* ```
|
|
1291
|
+
*
|
|
1292
|
+
* @example Async arrow function with generics
|
|
1293
|
+
* ```ts
|
|
1294
|
+
* createArrowFunction({
|
|
1295
|
+
* name: 'fetchPet',
|
|
1296
|
+
* export: true,
|
|
1297
|
+
* async: true,
|
|
1298
|
+
* generics: ['T'],
|
|
1299
|
+
* params: 'id: string',
|
|
1300
|
+
* returnType: 'T',
|
|
1301
|
+
* })
|
|
1302
|
+
* // export const fetchPet = async <T>(id: string): Promise<T> => { ... }
|
|
1303
|
+
* ```
|
|
157
1304
|
*/
|
|
158
|
-
|
|
1305
|
+
function createArrowFunction(props) {
|
|
1306
|
+
return {
|
|
1307
|
+
...props,
|
|
1308
|
+
kind: "ArrowFunction"
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
159
1311
|
/**
|
|
160
|
-
*
|
|
1312
|
+
* Creates a {@link TextNode} representing a raw string fragment in the source output.
|
|
1313
|
+
*
|
|
1314
|
+
* Use this instead of bare strings when building `nodes` arrays so that every
|
|
1315
|
+
* entry in the array is a typed {@link CodeNode}.
|
|
1316
|
+
*
|
|
1317
|
+
* @example
|
|
1318
|
+
* ```ts
|
|
1319
|
+
* createText('return fetch(id)')
|
|
1320
|
+
* // { kind: 'Text', value: 'return fetch(id)' }
|
|
1321
|
+
* ```
|
|
161
1322
|
*/
|
|
162
|
-
|
|
1323
|
+
function createText(value) {
|
|
1324
|
+
return {
|
|
1325
|
+
value,
|
|
1326
|
+
kind: "Text"
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
163
1329
|
/**
|
|
164
|
-
*
|
|
1330
|
+
* Creates a {@link BreakNode} representing a line break in the source output.
|
|
1331
|
+
*
|
|
1332
|
+
* Corresponds to `<br/>` in JSX components. Prints as an empty string which,
|
|
1333
|
+
* when joined with `\n` by `printNodes`, produces a blank line.
|
|
1334
|
+
*
|
|
1335
|
+
* @example
|
|
1336
|
+
* ```ts
|
|
1337
|
+
* createBreak()
|
|
1338
|
+
* // { kind: 'Break' }
|
|
1339
|
+
* ```
|
|
165
1340
|
*/
|
|
166
|
-
|
|
1341
|
+
function createBreak() {
|
|
1342
|
+
return { kind: "Break" };
|
|
1343
|
+
}
|
|
167
1344
|
/**
|
|
168
|
-
*
|
|
1345
|
+
* Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
|
|
1346
|
+
*
|
|
1347
|
+
* Use this to embed JSX markup (including fragments `<>…</>`) directly in generated code.
|
|
1348
|
+
*
|
|
1349
|
+
* @example
|
|
1350
|
+
* ```ts
|
|
1351
|
+
* createJsx('<>\n <a href={href}>Open</a>\n</>')
|
|
1352
|
+
* // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
|
|
1353
|
+
* ```
|
|
169
1354
|
*/
|
|
170
|
-
|
|
1355
|
+
function createJsx(value) {
|
|
1356
|
+
return {
|
|
1357
|
+
value,
|
|
1358
|
+
kind: "Jsx"
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
171
1361
|
//#endregion
|
|
172
1362
|
//#region src/printer.ts
|
|
173
1363
|
/**
|
|
174
|
-
* Creates a
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* object(node) {
|
|
192
|
-
* const props = node.properties
|
|
193
|
-
* ?.map(p => `${p.name}: ${this.print(p)}`)
|
|
194
|
-
* .join(', ') ?? ''
|
|
195
|
-
* return `z.object({ ${props} })`
|
|
196
|
-
* },
|
|
197
|
-
* },
|
|
198
|
-
* }
|
|
199
|
-
* })
|
|
1364
|
+
* Creates a schema printer factory.
|
|
1365
|
+
*
|
|
1366
|
+
* This function wraps a builder and makes options optional at call sites.
|
|
1367
|
+
*
|
|
1368
|
+
* The builder receives resolved options and returns:
|
|
1369
|
+
* - `name` — a unique identifier for the printer
|
|
1370
|
+
* - `options` — options stored on the returned printer instance
|
|
1371
|
+
* - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
|
|
1372
|
+
* - `print` _(optional)_ — top-level override exposed as `printer.print`
|
|
1373
|
+
* - Inside this function, use `this.transform(node)` to dispatch to the `nodes` map
|
|
1374
|
+
* - This keeps recursion safe and avoids self-calls
|
|
1375
|
+
*
|
|
1376
|
+
* When no `print` override is provided, `printer.print` falls back to `printer.transform` (the node-level dispatcher).
|
|
1377
|
+
*
|
|
1378
|
+
* @example Basic usage — Zod schema printer
|
|
1379
|
+
* ```ts
|
|
1380
|
+
* type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
|
|
200
1381
|
*
|
|
201
|
-
* const
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
1382
|
+
* export const zodPrinter = definePrinter<PrinterZod>((options) => ({
|
|
1383
|
+
* name: 'zod',
|
|
1384
|
+
* options: { strict: options.strict ?? true },
|
|
1385
|
+
* nodes: {
|
|
1386
|
+
* string: () => 'z.string()',
|
|
1387
|
+
* object(node) {
|
|
1388
|
+
* const props = node.properties.map(p => `${p.name}: ${this.transform(p.schema)}`).join(', ')
|
|
1389
|
+
* return `z.object({ ${props} })`
|
|
1390
|
+
* },
|
|
1391
|
+
* },
|
|
1392
|
+
* }))
|
|
205
1393
|
* ```
|
|
206
1394
|
*/
|
|
207
1395
|
function definePrinter(build) {
|
|
208
|
-
return (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
1396
|
+
return createPrinterFactory((node) => node.type)(build);
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Generic printer-factory function used by `definePrinter` and `defineFunctionPrinter`.
|
|
1400
|
+
**
|
|
1401
|
+
* @example
|
|
1402
|
+
* ```ts
|
|
1403
|
+
* export const defineFunctionPrinter = createPrinterFactory<FunctionNode, FunctionNodeType, FunctionNodeByType>(
|
|
1404
|
+
* (node) => kindToHandlerKey[node.kind],
|
|
1405
|
+
* )
|
|
1406
|
+
* ```
|
|
1407
|
+
*/
|
|
1408
|
+
function createPrinterFactory(getKey) {
|
|
1409
|
+
return function(build) {
|
|
1410
|
+
return (options) => {
|
|
1411
|
+
const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? {});
|
|
1412
|
+
const context = {
|
|
1413
|
+
options: resolvedOptions,
|
|
1414
|
+
transform: (node) => {
|
|
1415
|
+
const key = getKey(node);
|
|
1416
|
+
if (key === void 0) return null;
|
|
1417
|
+
const handler = nodes[key];
|
|
1418
|
+
if (!handler) return null;
|
|
1419
|
+
return handler.call(context, node);
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
return {
|
|
1423
|
+
name,
|
|
1424
|
+
options: resolvedOptions,
|
|
1425
|
+
transform: context.transform,
|
|
1426
|
+
print: printOverride ? printOverride.bind(context) : context.transform
|
|
1427
|
+
};
|
|
222
1428
|
};
|
|
223
1429
|
};
|
|
224
1430
|
}
|
|
225
1431
|
//#endregion
|
|
226
1432
|
//#region src/refs.ts
|
|
227
1433
|
/**
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
* Looks up a schema by name. Prefer over `RefMap.get()` to keep the resolution strategy swappable.
|
|
237
|
-
*/
|
|
238
|
-
function resolveRef(refMap, ref) {
|
|
239
|
-
return refMap.get(ref);
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Converts a `RefMap` to a plain object.
|
|
1434
|
+
* Returns the last path segment of a reference string.
|
|
1435
|
+
*
|
|
1436
|
+
* Example: `#/components/schemas/Pet` becomes `Pet`.
|
|
1437
|
+
*
|
|
1438
|
+
* @example
|
|
1439
|
+
* ```ts
|
|
1440
|
+
* extractRefName('#/components/schemas/Pet') // 'Pet'
|
|
1441
|
+
* ```
|
|
243
1442
|
*/
|
|
244
|
-
function
|
|
245
|
-
return
|
|
1443
|
+
function extractRefName(ref) {
|
|
1444
|
+
return ref.split("/").at(-1) ?? ref;
|
|
246
1445
|
}
|
|
247
1446
|
//#endregion
|
|
248
|
-
//#region src/
|
|
249
|
-
const plainStringTypes = new Set([
|
|
250
|
-
"string",
|
|
251
|
-
"uuid",
|
|
252
|
-
"email",
|
|
253
|
-
"url",
|
|
254
|
-
"datetime"
|
|
255
|
-
]);
|
|
1447
|
+
//#region src/visitor.ts
|
|
256
1448
|
/**
|
|
257
|
-
*
|
|
1449
|
+
* Creates a small async concurrency limiter.
|
|
258
1450
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
1451
|
+
* At most `concurrency` tasks are in flight at once. Extra tasks are queued.
|
|
1452
|
+
*
|
|
1453
|
+
* @example
|
|
1454
|
+
* ```ts
|
|
1455
|
+
* const limit = createLimit(2)
|
|
1456
|
+
* for (const task of [taskA, taskB, taskC]) {
|
|
1457
|
+
* await limit(() => task())
|
|
1458
|
+
* }
|
|
1459
|
+
* // only 2 tasks run at the same time
|
|
1460
|
+
* ```
|
|
261
1461
|
*/
|
|
262
|
-
function isPlainStringType(node) {
|
|
263
|
-
if (plainStringTypes.has(node.type)) return true;
|
|
264
|
-
const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
|
|
265
|
-
if (temporal) return temporal.representation !== "date";
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
//#endregion
|
|
269
|
-
//#region src/visitor.ts
|
|
270
1462
|
function createLimit(concurrency) {
|
|
271
1463
|
let active = 0;
|
|
272
1464
|
const queue = [];
|
|
@@ -289,14 +1481,25 @@ function createLimit(concurrency) {
|
|
|
289
1481
|
};
|
|
290
1482
|
}
|
|
291
1483
|
/**
|
|
292
|
-
*
|
|
1484
|
+
* Returns the immediate traversable children of `node`.
|
|
1485
|
+
*
|
|
1486
|
+
* For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
|
|
1487
|
+
* `additionalProperties`) are only included
|
|
1488
|
+
* when `recurse` is `true`; shallow mode skips them.
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```ts
|
|
1492
|
+
* const children = getChildren(operationNode, true)
|
|
1493
|
+
* // returns parameters, requestBody schema (if present), and responses
|
|
1494
|
+
* ```
|
|
293
1495
|
*/
|
|
294
1496
|
function getChildren(node, recurse) {
|
|
295
1497
|
switch (node.kind) {
|
|
296
|
-
case "
|
|
1498
|
+
case "Input": return [...node.schemas, ...node.operations];
|
|
1499
|
+
case "Output": return [];
|
|
297
1500
|
case "Operation": return [
|
|
298
1501
|
...node.parameters,
|
|
299
|
-
...node.requestBody ? [node.requestBody] : [],
|
|
1502
|
+
...node.requestBody?.schema ? [node.requestBody.schema] : [],
|
|
300
1503
|
...node.responses
|
|
301
1504
|
];
|
|
302
1505
|
case "Schema": {
|
|
@@ -305,140 +1508,374 @@ function getChildren(node, recurse) {
|
|
|
305
1508
|
if ("properties" in node && node.properties.length > 0) children.push(...node.properties);
|
|
306
1509
|
if ("items" in node && node.items) children.push(...node.items);
|
|
307
1510
|
if ("members" in node && node.members) children.push(...node.members);
|
|
1511
|
+
if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
|
|
308
1512
|
return children;
|
|
309
1513
|
}
|
|
310
1514
|
case "Property": return [node.schema];
|
|
311
1515
|
case "Parameter": return [node.schema];
|
|
312
1516
|
case "Response": return node.schema ? [node.schema] : [];
|
|
1517
|
+
case "FunctionParameter":
|
|
1518
|
+
case "ParameterGroup":
|
|
1519
|
+
case "FunctionParameters":
|
|
1520
|
+
case "Type": return [];
|
|
1521
|
+
default: return [];
|
|
313
1522
|
}
|
|
314
1523
|
}
|
|
315
1524
|
/**
|
|
316
1525
|
* Depth-first traversal for side effects. Visitor return values are ignored.
|
|
317
|
-
* Sibling nodes at each level are visited concurrently up to `options.concurrency`
|
|
1526
|
+
* Sibling nodes at each level are visited concurrently up to `options.concurrency`
|
|
1527
|
+
* (default: `WALK_CONCURRENCY`).
|
|
1528
|
+
*
|
|
1529
|
+
* @example
|
|
1530
|
+
* ```ts
|
|
1531
|
+
* await walk(root, {
|
|
1532
|
+
* operation(node) {
|
|
1533
|
+
* console.log(node.operationId)
|
|
1534
|
+
* },
|
|
1535
|
+
* })
|
|
1536
|
+
* ```
|
|
1537
|
+
*
|
|
1538
|
+
* @example
|
|
1539
|
+
* ```ts
|
|
1540
|
+
* // Visit only the current node
|
|
1541
|
+
* await walk(root, { depth: 'shallow', root: () => {} })
|
|
1542
|
+
* ```
|
|
318
1543
|
*/
|
|
319
|
-
async function walk(node,
|
|
320
|
-
return _walk(node,
|
|
1544
|
+
async function walk(node, options) {
|
|
1545
|
+
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
321
1546
|
}
|
|
322
|
-
async function _walk(node, visitor, recurse, limit) {
|
|
1547
|
+
async function _walk(node, visitor, recurse, limit, parent) {
|
|
323
1548
|
switch (node.kind) {
|
|
324
|
-
case "
|
|
325
|
-
await limit(() => visitor.
|
|
1549
|
+
case "Input":
|
|
1550
|
+
await limit(() => visitor.input?.(node, { parent }));
|
|
1551
|
+
break;
|
|
1552
|
+
case "Output":
|
|
1553
|
+
await limit(() => visitor.output?.(node, { parent }));
|
|
326
1554
|
break;
|
|
327
1555
|
case "Operation":
|
|
328
|
-
await limit(() => visitor.operation?.(node));
|
|
1556
|
+
await limit(() => visitor.operation?.(node, { parent }));
|
|
329
1557
|
break;
|
|
330
1558
|
case "Schema":
|
|
331
|
-
await limit(() => visitor.schema?.(node));
|
|
1559
|
+
await limit(() => visitor.schema?.(node, { parent }));
|
|
332
1560
|
break;
|
|
333
1561
|
case "Property":
|
|
334
|
-
await limit(() => visitor.property?.(node));
|
|
1562
|
+
await limit(() => visitor.property?.(node, { parent }));
|
|
335
1563
|
break;
|
|
336
1564
|
case "Parameter":
|
|
337
|
-
await limit(() => visitor.parameter?.(node));
|
|
1565
|
+
await limit(() => visitor.parameter?.(node, { parent }));
|
|
338
1566
|
break;
|
|
339
1567
|
case "Response":
|
|
340
|
-
await limit(() => visitor.response?.(node));
|
|
1568
|
+
await limit(() => visitor.response?.(node, { parent }));
|
|
341
1569
|
break;
|
|
1570
|
+
case "FunctionParameter":
|
|
1571
|
+
case "ParameterGroup":
|
|
1572
|
+
case "FunctionParameters": break;
|
|
342
1573
|
}
|
|
343
1574
|
const children = getChildren(node, recurse);
|
|
344
|
-
|
|
1575
|
+
for (const child of children) await _walk(child, visitor, recurse, limit, node);
|
|
345
1576
|
}
|
|
346
|
-
function transform(node,
|
|
347
|
-
const
|
|
1577
|
+
function transform(node, options) {
|
|
1578
|
+
const { depth, parent, ...visitor } = options;
|
|
1579
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
348
1580
|
switch (node.kind) {
|
|
349
|
-
case "
|
|
350
|
-
let
|
|
351
|
-
const replaced = visitor.
|
|
352
|
-
if (replaced)
|
|
1581
|
+
case "Input": {
|
|
1582
|
+
let input = node;
|
|
1583
|
+
const replaced = visitor.input?.(input, { parent });
|
|
1584
|
+
if (replaced) input = replaced;
|
|
353
1585
|
return {
|
|
354
|
-
...
|
|
355
|
-
schemas:
|
|
356
|
-
|
|
1586
|
+
...input,
|
|
1587
|
+
schemas: input.schemas.map((s) => transform(s, {
|
|
1588
|
+
...options,
|
|
1589
|
+
parent: input
|
|
1590
|
+
})),
|
|
1591
|
+
operations: input.operations.map((op) => transform(op, {
|
|
1592
|
+
...options,
|
|
1593
|
+
parent: input
|
|
1594
|
+
}))
|
|
357
1595
|
};
|
|
358
1596
|
}
|
|
1597
|
+
case "Output": {
|
|
1598
|
+
let output = node;
|
|
1599
|
+
const replaced = visitor.output?.(output, { parent });
|
|
1600
|
+
if (replaced) output = replaced;
|
|
1601
|
+
return output;
|
|
1602
|
+
}
|
|
359
1603
|
case "Operation": {
|
|
360
1604
|
let op = node;
|
|
361
|
-
const replaced = visitor.operation?.(op);
|
|
1605
|
+
const replaced = visitor.operation?.(op, { parent });
|
|
362
1606
|
if (replaced) op = replaced;
|
|
363
1607
|
return {
|
|
364
1608
|
...op,
|
|
365
|
-
parameters: op.parameters.map((p) => transform(p,
|
|
366
|
-
|
|
367
|
-
|
|
1609
|
+
parameters: op.parameters.map((p) => transform(p, {
|
|
1610
|
+
...options,
|
|
1611
|
+
parent: op
|
|
1612
|
+
})),
|
|
1613
|
+
requestBody: op.requestBody ? {
|
|
1614
|
+
...op.requestBody,
|
|
1615
|
+
schema: op.requestBody.schema ? transform(op.requestBody.schema, {
|
|
1616
|
+
...options,
|
|
1617
|
+
parent: op
|
|
1618
|
+
}) : void 0
|
|
1619
|
+
} : void 0,
|
|
1620
|
+
responses: op.responses.map((r) => transform(r, {
|
|
1621
|
+
...options,
|
|
1622
|
+
parent: op
|
|
1623
|
+
}))
|
|
368
1624
|
};
|
|
369
1625
|
}
|
|
370
1626
|
case "Schema": {
|
|
371
1627
|
let schema = node;
|
|
372
|
-
const replaced = visitor.schema?.(schema);
|
|
1628
|
+
const replaced = visitor.schema?.(schema, { parent });
|
|
373
1629
|
if (replaced) schema = replaced;
|
|
1630
|
+
const childOptions = {
|
|
1631
|
+
...options,
|
|
1632
|
+
parent: schema
|
|
1633
|
+
};
|
|
374
1634
|
return {
|
|
375
1635
|
...schema,
|
|
376
|
-
..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p,
|
|
377
|
-
..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i,
|
|
378
|
-
..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m,
|
|
1636
|
+
..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
|
|
1637
|
+
..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
|
|
1638
|
+
..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
|
|
1639
|
+
..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
|
|
379
1640
|
};
|
|
380
1641
|
}
|
|
381
1642
|
case "Property": {
|
|
382
1643
|
let prop = node;
|
|
383
|
-
const replaced = visitor.property?.(prop);
|
|
1644
|
+
const replaced = visitor.property?.(prop, { parent });
|
|
384
1645
|
if (replaced) prop = replaced;
|
|
385
|
-
return {
|
|
1646
|
+
return createProperty({
|
|
386
1647
|
...prop,
|
|
387
|
-
schema: transform(prop.schema,
|
|
388
|
-
|
|
1648
|
+
schema: transform(prop.schema, {
|
|
1649
|
+
...options,
|
|
1650
|
+
parent: prop
|
|
1651
|
+
})
|
|
1652
|
+
});
|
|
389
1653
|
}
|
|
390
1654
|
case "Parameter": {
|
|
391
1655
|
let param = node;
|
|
392
|
-
const replaced = visitor.parameter?.(param);
|
|
1656
|
+
const replaced = visitor.parameter?.(param, { parent });
|
|
393
1657
|
if (replaced) param = replaced;
|
|
394
|
-
return {
|
|
1658
|
+
return createParameter({
|
|
395
1659
|
...param,
|
|
396
|
-
schema: transform(param.schema,
|
|
397
|
-
|
|
1660
|
+
schema: transform(param.schema, {
|
|
1661
|
+
...options,
|
|
1662
|
+
parent: param
|
|
1663
|
+
})
|
|
1664
|
+
});
|
|
398
1665
|
}
|
|
399
1666
|
case "Response": {
|
|
400
1667
|
let response = node;
|
|
401
|
-
const replaced = visitor.response?.(response);
|
|
1668
|
+
const replaced = visitor.response?.(response, { parent });
|
|
402
1669
|
if (replaced) response = replaced;
|
|
403
1670
|
return {
|
|
404
1671
|
...response,
|
|
405
|
-
schema:
|
|
1672
|
+
schema: transform(response.schema, {
|
|
1673
|
+
...options,
|
|
1674
|
+
parent: response
|
|
1675
|
+
})
|
|
406
1676
|
};
|
|
407
1677
|
}
|
|
1678
|
+
case "FunctionParameter":
|
|
1679
|
+
case "ParameterGroup":
|
|
1680
|
+
case "FunctionParameters":
|
|
1681
|
+
case "Type": return node;
|
|
1682
|
+
default: return node;
|
|
408
1683
|
}
|
|
409
1684
|
}
|
|
410
1685
|
/**
|
|
411
|
-
*
|
|
1686
|
+
* Runs a depth-first synchronous collection pass.
|
|
1687
|
+
*
|
|
1688
|
+
* Non-`undefined` values returned by visitor callbacks are appended to the result.
|
|
1689
|
+
*
|
|
1690
|
+
* @example
|
|
1691
|
+
* ```ts
|
|
1692
|
+
* const ids = collect(root, {
|
|
1693
|
+
* operation(node) {
|
|
1694
|
+
* return node.operationId
|
|
1695
|
+
* },
|
|
1696
|
+
* })
|
|
1697
|
+
* ```
|
|
1698
|
+
*
|
|
1699
|
+
* @example
|
|
1700
|
+
* ```ts
|
|
1701
|
+
* // Collect from only the current node
|
|
1702
|
+
* const values = collect(root, { depth: 'shallow', root: () => 'root' })
|
|
1703
|
+
* ```
|
|
412
1704
|
*/
|
|
413
|
-
function collect(node,
|
|
414
|
-
const
|
|
1705
|
+
function collect(node, options) {
|
|
1706
|
+
const { depth, parent, ...visitor } = options;
|
|
1707
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
415
1708
|
const results = [];
|
|
416
1709
|
let v;
|
|
417
1710
|
switch (node.kind) {
|
|
418
|
-
case "
|
|
419
|
-
v = visitor.
|
|
1711
|
+
case "Input":
|
|
1712
|
+
v = visitor.input?.(node, { parent });
|
|
1713
|
+
break;
|
|
1714
|
+
case "Output":
|
|
1715
|
+
v = visitor.output?.(node, { parent });
|
|
420
1716
|
break;
|
|
421
1717
|
case "Operation":
|
|
422
|
-
v = visitor.operation?.(node);
|
|
1718
|
+
v = visitor.operation?.(node, { parent });
|
|
423
1719
|
break;
|
|
424
1720
|
case "Schema":
|
|
425
|
-
v = visitor.schema?.(node);
|
|
1721
|
+
v = visitor.schema?.(node, { parent });
|
|
426
1722
|
break;
|
|
427
1723
|
case "Property":
|
|
428
|
-
v = visitor.property?.(node);
|
|
1724
|
+
v = visitor.property?.(node, { parent });
|
|
429
1725
|
break;
|
|
430
1726
|
case "Parameter":
|
|
431
|
-
v = visitor.parameter?.(node);
|
|
1727
|
+
v = visitor.parameter?.(node, { parent });
|
|
432
1728
|
break;
|
|
433
1729
|
case "Response":
|
|
434
|
-
v = visitor.response?.(node);
|
|
1730
|
+
v = visitor.response?.(node, { parent });
|
|
435
1731
|
break;
|
|
1732
|
+
case "FunctionParameter":
|
|
1733
|
+
case "ParameterGroup":
|
|
1734
|
+
case "FunctionParameters": break;
|
|
436
1735
|
}
|
|
437
1736
|
if (v !== void 0) results.push(v);
|
|
438
|
-
for (const child of getChildren(node, recurse)) for (const item of collect(child,
|
|
1737
|
+
for (const child of getChildren(node, recurse)) for (const item of collect(child, {
|
|
1738
|
+
...options,
|
|
1739
|
+
parent: node
|
|
1740
|
+
})) results.push(item);
|
|
439
1741
|
return results;
|
|
440
1742
|
}
|
|
441
1743
|
//#endregion
|
|
442
|
-
|
|
1744
|
+
//#region src/resolvers.ts
|
|
1745
|
+
function findDiscriminator(mapping, ref) {
|
|
1746
|
+
if (!mapping || !ref) return null;
|
|
1747
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
1748
|
+
}
|
|
1749
|
+
function childName(parentName, propName) {
|
|
1750
|
+
return parentName ? pascalCase([parentName, propName].join(" ")) : null;
|
|
1751
|
+
}
|
|
1752
|
+
function enumPropName(parentName, propName, enumSuffix) {
|
|
1753
|
+
return pascalCase([
|
|
1754
|
+
parentName,
|
|
1755
|
+
propName,
|
|
1756
|
+
enumSuffix
|
|
1757
|
+
].filter(Boolean).join(" "));
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Collects import entries for all `ref` schema nodes in `node`.
|
|
1761
|
+
*/
|
|
1762
|
+
function collectImports({ node, nameMapping, resolve }) {
|
|
1763
|
+
return collect(node, { schema(schemaNode) {
|
|
1764
|
+
const schemaRef = narrowSchema(schemaNode, "ref");
|
|
1765
|
+
if (!schemaRef?.ref) return;
|
|
1766
|
+
const rawName = extractRefName(schemaRef.ref);
|
|
1767
|
+
const result = resolve(nameMapping.get(rawName) ?? rawName);
|
|
1768
|
+
if (!result) return;
|
|
1769
|
+
return result;
|
|
1770
|
+
} });
|
|
1771
|
+
}
|
|
1772
|
+
//#endregion
|
|
1773
|
+
//#region src/transformers.ts
|
|
1774
|
+
/**
|
|
1775
|
+
* Replaces a discriminator property's schema with a string enum of allowed values.
|
|
1776
|
+
*
|
|
1777
|
+
* If `node` is not an object schema, or if the property does not exist, the input
|
|
1778
|
+
* node is returned as-is.
|
|
1779
|
+
*
|
|
1780
|
+
* @example
|
|
1781
|
+
* ```ts
|
|
1782
|
+
* const schema = createSchema({
|
|
1783
|
+
* type: 'object',
|
|
1784
|
+
* properties: [createProperty({ name: 'type', required: true, schema: createSchema({ type: 'string' }) })],
|
|
1785
|
+
* })
|
|
1786
|
+
* const result = setDiscriminatorEnum({ node: schema, propertyName: 'type', values: ['dog', 'cat'] })
|
|
1787
|
+
* ```
|
|
1788
|
+
*/
|
|
1789
|
+
function setDiscriminatorEnum({ node, propertyName, values, enumName }) {
|
|
1790
|
+
const objectNode = narrowSchema(node, "object");
|
|
1791
|
+
if (!objectNode?.properties?.length) return node;
|
|
1792
|
+
if (!objectNode.properties.some((prop) => prop.name === propertyName)) return node;
|
|
1793
|
+
return createSchema({
|
|
1794
|
+
...objectNode,
|
|
1795
|
+
properties: objectNode.properties.map((prop) => {
|
|
1796
|
+
if (prop.name !== propertyName) return prop;
|
|
1797
|
+
return createProperty({
|
|
1798
|
+
...prop,
|
|
1799
|
+
schema: createSchema({
|
|
1800
|
+
type: "enum",
|
|
1801
|
+
primitive: "string",
|
|
1802
|
+
enumValues: values,
|
|
1803
|
+
name: enumName,
|
|
1804
|
+
readOnly: prop.schema.readOnly,
|
|
1805
|
+
writeOnly: prop.schema.writeOnly
|
|
1806
|
+
})
|
|
1807
|
+
});
|
|
1808
|
+
})
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Merges adjacent anonymous object members into a single anonymous object member.
|
|
1813
|
+
*
|
|
1814
|
+
* @example
|
|
1815
|
+
* ```ts
|
|
1816
|
+
* const merged = mergeAdjacentObjects([
|
|
1817
|
+
* createSchema({ type: 'object', properties: [createProperty({ name: 'a', schema: createSchema({ type: 'string' }) })] }),
|
|
1818
|
+
* createSchema({ type: 'object', properties: [createProperty({ name: 'b', schema: createSchema({ type: 'number' }) })] }),
|
|
1819
|
+
* ])
|
|
1820
|
+
* ```
|
|
1821
|
+
*/
|
|
1822
|
+
function mergeAdjacentObjects(members) {
|
|
1823
|
+
return members.reduce((acc, member) => {
|
|
1824
|
+
const objectMember = narrowSchema(member, "object");
|
|
1825
|
+
if (objectMember && !objectMember.name) {
|
|
1826
|
+
const previous = acc.at(-1);
|
|
1827
|
+
const previousObject = previous ? narrowSchema(previous, "object") : void 0;
|
|
1828
|
+
if (previousObject && !previousObject.name) {
|
|
1829
|
+
acc[acc.length - 1] = createSchema({
|
|
1830
|
+
...previousObject,
|
|
1831
|
+
properties: [...previousObject.properties ?? [], ...objectMember.properties ?? []]
|
|
1832
|
+
});
|
|
1833
|
+
return acc;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
acc.push(member);
|
|
1837
|
+
return acc;
|
|
1838
|
+
}, []);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Removes enum members that are covered by broader scalar primitives in the same union.
|
|
1842
|
+
*
|
|
1843
|
+
* @example
|
|
1844
|
+
* ```ts
|
|
1845
|
+
* const simplified = simplifyUnion([
|
|
1846
|
+
* createSchema({ type: 'enum', primitive: 'string', enumValues: ['active'] }),
|
|
1847
|
+
* createSchema({ type: 'string' }),
|
|
1848
|
+
* ])
|
|
1849
|
+
* // keeps only string member
|
|
1850
|
+
* ```
|
|
1851
|
+
*/
|
|
1852
|
+
function simplifyUnion(members) {
|
|
1853
|
+
const scalarPrimitives = new Set(members.filter((member) => isScalarPrimitive(member.type)).map((m) => m.type));
|
|
1854
|
+
if (!scalarPrimitives.size) return members;
|
|
1855
|
+
return members.filter((member) => {
|
|
1856
|
+
const enumNode = narrowSchema(member, "enum");
|
|
1857
|
+
if (!enumNode) return true;
|
|
1858
|
+
const primitive = enumNode.primitive;
|
|
1859
|
+
if (!primitive) return true;
|
|
1860
|
+
if ((enumNode.namedEnumValues?.length ?? enumNode.enumValues?.length ?? 0) <= 1) return true;
|
|
1861
|
+
if (scalarPrimitives.has(primitive)) return false;
|
|
1862
|
+
if ((primitive === "integer" || primitive === "number") && (scalarPrimitives.has("integer") || scalarPrimitives.has("number"))) return false;
|
|
1863
|
+
return true;
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
function setEnumName(propNode, parentName, propName, enumSuffix) {
|
|
1867
|
+
const enumNode = narrowSchema(propNode, "enum");
|
|
1868
|
+
if (enumNode?.primitive === "boolean") return {
|
|
1869
|
+
...propNode,
|
|
1870
|
+
name: void 0
|
|
1871
|
+
};
|
|
1872
|
+
if (enumNode) return {
|
|
1873
|
+
...propNode,
|
|
1874
|
+
name: enumPropName(parentName, propName, enumSuffix)
|
|
1875
|
+
};
|
|
1876
|
+
return propNode;
|
|
1877
|
+
}
|
|
1878
|
+
//#endregion
|
|
1879
|
+
export { caseParams, childName, collect, collectImports, createArrowFunction, createBreak, createConst, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter, createParameterGroup, createParamsType, createPrinterFactory, createProperty, createResponse, createSchema, createSource, createText, createType, definePrinter, enumPropName, extractRefName, extractStringsFromNodes, findDiscriminator, httpMethods, isInputNode, isOperationNode, isOutputNode, isScalarPrimitive, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, narrowSchema, nodeKinds, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, walk };
|
|
443
1880
|
|
|
444
1881
|
//# sourceMappingURL=index.js.map
|