@kubb/ast 5.0.0-alpha.31 → 5.0.0-alpha.32

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