@kubb/ast 5.0.0-alpha.1 → 5.0.0-alpha.10

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/src/printer.ts CHANGED
@@ -2,11 +2,12 @@ import type { SchemaNode, SchemaNodeByType, SchemaType } from './nodes/index.ts'
2
2
 
3
3
  /**
4
4
  * Handler context for `definePrinter` — mirrors `PluginContext` from `@kubb/core`.
5
- * Available as `this` inside each node handler.
5
+ * Available as `this` inside each node handler and the optional root-level `print`.
6
+ * `this.print` always dispatches to the `nodes` handlers (node-level printer).
6
7
  */
7
8
  export type PrinterHandlerContext<TOutput, TOptions extends object> = {
8
9
  /**
9
- * Recursively print a nested `SchemaNode`.
10
+ * Recursively print a nested `SchemaNode` using the node-level handlers.
10
11
  */
11
12
  print: (node: SchemaNode) => TOutput | null | undefined
12
13
  /**
@@ -28,19 +29,20 @@ export type PrinterHandler<TOutput, TOptions extends object, T extends SchemaTyp
28
29
  * Shape of the type parameter passed to `definePrinter`.
29
30
  * Mirrors `AdapterFactoryOptions` / `PluginFactoryOptions` from `@kubb/core`.
30
31
  *
31
- * - `TName` — unique string identifier (e.g. `'zod'`, `'ts'`)
32
- * - `TOptions` — options passed to and stored on the printer
33
- * - `TOutput` — the type emitted by `print` (typically `string`)
32
+ * - `TName` — unique string identifier (e.g. `'zod'`, `'ts'`)
33
+ * - `TOptions` — options passed to and stored on the printer
34
+ * - `TOutput` — the type emitted by node handlers
35
+ * - `TPrintOutput` — the type emitted by the public `print` override (defaults to `TOutput`)
34
36
  */
35
- export type PrinterFactoryOptions<TName extends string = string, TOptions extends object = object, TOutput = unknown> = {
37
+ export type PrinterFactoryOptions<TName extends string = string, TOptions extends object = object, TOutput = unknown, TPrintOutput = TOutput> = {
36
38
  name: TName
37
39
  options: TOptions
38
40
  output: TOutput
41
+ printOutput: TPrintOutput
39
42
  }
40
43
 
41
44
  /**
42
45
  * The object returned by calling a `definePrinter` instance.
43
- * Mirrors the shape of `Adapter` from `@kubb/core`.
44
46
  */
45
47
  export type Printer<T extends PrinterFactoryOptions = PrinterFactoryOptions> = {
46
48
  /**
@@ -52,15 +54,18 @@ export type Printer<T extends PrinterFactoryOptions = PrinterFactoryOptions> = {
52
54
  */
53
55
  options: T['options']
54
56
  /**
55
- * Emits `TOutput` from a `SchemaNode`. Returns `null | undefined` when no handler matches.
57
+ * Public printer. If the builder provides a root-level `print`, this calls that
58
+ * higher-level function (which may produce full declarations). Otherwise falls back
59
+ * to the node-level dispatcher
56
60
  */
57
- print: (node: SchemaNode) => T['output'] | null | undefined
58
- /**
59
- * Maps `print` over an array of `SchemaNode`s.
60
- */
61
- for: (nodes: Array<SchemaNode>) => Array<T['output'] | null | undefined>
61
+ print: (node: SchemaNode) => T['printOutput'] | null | undefined
62
62
  }
63
63
 
64
+ /**
65
+ * Builder function passed to `definePrinter`. Receives the resolved options and returns the
66
+ * printer configuration: a unique `name`, the stored `options`, node-level `nodes` handlers,
67
+ * and an optional root-level `print` override.
68
+ */
64
69
  type PrinterBuilder<T extends PrinterFactoryOptions> = (options: T['options']) => {
65
70
  name: T['name']
66
71
  /**
@@ -70,60 +75,114 @@ type PrinterBuilder<T extends PrinterFactoryOptions> = (options: T['options']) =
70
75
  nodes: Partial<{
71
76
  [K in SchemaType]: PrinterHandler<T['output'], T['options'], K>
72
77
  }>
78
+ /**
79
+ * Optional root-level print override. When provided, becomes the public `printer.print`.
80
+ * `this.print(node)` inside this function calls the node-level dispatcher (`nodes` handlers),
81
+ * not the override itself — so recursion is safe.
82
+ */
83
+ print?: (this: PrinterHandlerContext<T['output'], T['options']>, node: SchemaNode) => T['printOutput'] | null | undefined
73
84
  }
74
85
 
75
86
  /**
76
- * Creates a named printer factory. Mirrors the `definePlugin` / `defineAdapter` pattern
87
+ * Creates a named printer factory. Mirrors the `createPlugin` / `createAdapter` pattern
77
88
  * from `@kubb/core` — wraps a builder to make options optional and separates raw options
78
89
  * from resolved options.
79
90
  *
80
- * @example
91
+ * The builder receives resolved options and returns:
92
+ * - `name` — a unique identifier for the printer
93
+ * - `options` — options stored on the returned printer instance
94
+ * - `nodes` — a map of `SchemaType` → handler functions that convert a `SchemaNode` to `TOutput`
95
+ * - `print` _(optional)_ — a root-level override that becomes the public `printer.print`.
96
+ * Inside it, `this.print(node)` still dispatches to the `nodes` map — safe recursion, no infinite loop.
97
+ *
98
+ * When no `print` override is provided, `printer.print` is the node-level dispatcher directly.
99
+ *
100
+ * @example Basic usage — Zod schema printer
81
101
  * ```ts
82
- * type ZodPrinter = PrinterFactoryOptions<'zod', { strict?: boolean }, { strict: boolean }, string>
102
+ * type ZodPrinter = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
83
103
  *
84
- * export const zodPrinter = definePrinter<ZodPrinter>((options) => {
85
- * const { strict = true } = options
86
- * return {
87
- * name: 'zod',
88
- * options: { strict },
89
- * nodes: {
90
- * string(node) {
91
- * return `z.string()`
92
- * },
93
- * object(node) {
94
- * const props = node.properties
95
- * ?.map(p => `${p.name}: ${this.print(p)}`)
96
- * .join(', ') ?? ''
97
- * return `z.object({ ${props} })`
98
- * },
104
+ * export const zodPrinter = definePrinter<ZodPrinter>((options) => ({
105
+ * name: 'zod',
106
+ * options: { strict: options.strict ?? true },
107
+ * nodes: {
108
+ * string: () => 'z.string()',
109
+ * object(node) {
110
+ * const props = node.properties.map(p => `${p.name}: ${this.print(p.schema)}`).join(', ')
111
+ * return `z.object({ ${props} })`
99
112
  * },
100
- * }
101
- * })
113
+ * },
114
+ * }))
115
+ * ```
102
116
  *
103
- * const printer = zodPrinter({ strict: false })
104
- * printer.name // 'zod'
105
- * printer.options // { strict: false }
106
- * printer.print(node) // 'z.string()'
117
+ * @example With a root-level `print` override to wrap output in a full declaration
118
+ * ```ts
119
+ * type TsPrinter = PrinterFactoryOptions<'ts', { typeName?: string }, ts.TypeNode, ts.Node>
120
+ *
121
+ * export const printerTs = definePrinter<TsPrinter>((options) => ({
122
+ * name: 'ts',
123
+ * options,
124
+ * nodes: { string: () => factory.keywordTypeNodes.string },
125
+ * print(node) {
126
+ * const type = this.print(node) // calls the node-level dispatcher
127
+ * if (!type || !this.options.typeName) return type
128
+ * return factory.createTypeAliasDeclaration(this.options.typeName, type)
129
+ * },
130
+ * }))
107
131
  * ```
108
132
  */
109
133
  export function definePrinter<T extends PrinterFactoryOptions = PrinterFactoryOptions>(build: PrinterBuilder<T>): (options?: T['options']) => Printer<T> {
110
- return (options) => {
111
- const { name, options: resolvedOptions, nodes } = build(options ?? ({} as T['options']))
134
+ return createPrinterFactory<SchemaNode, SchemaType, SchemaNodeByType>((node) => node.type)(build) as (options?: T['options']) => Printer<T>
135
+ }
112
136
 
113
- const context: PrinterHandlerContext<T['output'], T['options']> = {
114
- options: resolvedOptions,
115
- print: (node: SchemaNode) => {
116
- const type = node.type as SchemaType
117
- const handler = nodes[type]
118
- return handler ? (handler as PrinterHandler<T['output'], T['options']>).call(context, node as SchemaNodeByType[SchemaType]) : undefined
119
- },
120
- }
137
+ /**
138
+ * Generic printer factory. Extracts the core dispatch + context logic so it can be reused
139
+ * for any node type — not just `SchemaNode`. `definePrinter` is built on top of this.
140
+ *
141
+ * @param getKey — derives the handler-map key from a node. Return `undefined` to skip.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * export const defineFunctionPrinter = createPrinterFactory<FunctionNode, FunctionNodeType, FunctionNodeByType>(
146
+ * (node) => kindToHandlerKey[node.kind],
147
+ * )
148
+ * ```
149
+ */
150
+ export function createPrinterFactory<TNode, TKey extends string, TNodeByKey extends Partial<Record<TKey, TNode>>>(getKey: (node: TNode) => TKey | undefined) {
151
+ return function <T extends PrinterFactoryOptions>(
152
+ build: (options: T['options']) => {
153
+ name: T['name']
154
+ options: T['options']
155
+ nodes: Partial<{
156
+ [K in TKey]: (
157
+ this: { print: (node: TNode) => T['output'] | null | undefined; options: T['options'] },
158
+ node: TNodeByKey[K],
159
+ ) => T['output'] | null | undefined
160
+ }>
161
+ print?: (this: { print: (node: TNode) => T['output'] | null | undefined; options: T['options'] }, node: TNode) => T['printOutput'] | null | undefined
162
+ },
163
+ ): (options?: T['options']) => { name: T['name']; options: T['options']; print: (node: TNode) => T['printOutput'] | null | undefined } {
164
+ return (options) => {
165
+ const { name, options: resolvedOptions, nodes, print: printOverride } = build(options ?? ({} as T['options']))
166
+
167
+ const context = {
168
+ options: resolvedOptions,
169
+ print: (node: TNode): T['output'] | null | undefined => {
170
+ const key = getKey(node)
171
+ if (key === undefined) return undefined
172
+
173
+ const handler = nodes[key]
174
+
175
+ if (!handler) return undefined
176
+
177
+ return (handler as (this: typeof context, node: TNode) => T['output'] | null | undefined).call(context, node)
178
+ },
179
+ }
121
180
 
122
- return {
123
- name,
124
- options: resolvedOptions,
125
- print: context.print,
126
- for: (nodes) => nodes.map(context.print),
181
+ return {
182
+ name,
183
+ options: resolvedOptions,
184
+ print: (printOverride ? printOverride.bind(context) : context.print) as (node: TNode) => T['printOutput'] | null | undefined,
185
+ }
127
186
  }
128
187
  }
129
188
  }
package/src/types.ts CHANGED
@@ -8,6 +8,10 @@ export type {
8
8
  DatetimeSchemaNode,
9
9
  EnumSchemaNode,
10
10
  EnumValueNode,
11
+ FunctionNode,
12
+ FunctionNodeType,
13
+ FunctionParameterNode,
14
+ FunctionParametersNode,
11
15
  HttpMethod,
12
16
  HttpStatusCode,
13
17
  IntersectionSchemaNode,
@@ -15,6 +19,7 @@ export type {
15
19
  Node,
16
20
  NodeKind,
17
21
  NumberSchemaNode,
22
+ ObjectBindingParameterNode,
18
23
  ObjectSchemaNode,
19
24
  OperationNode,
20
25
  ParameterLocation,
@@ -35,7 +40,8 @@ export type {
35
40
  StringSchemaNode,
36
41
  TimeSchemaNode,
37
42
  UnionSchemaNode,
43
+ UrlSchemaNode,
38
44
  } from './nodes/index.ts'
39
- export type { Printer, PrinterFactoryOptions, PrinterHandler, PrinterHandlerContext } from './printer.ts'
45
+ export type { Printer, PrinterFactoryOptions } from './printer.ts'
40
46
  export type { RefMap } from './refs.ts'
41
47
  export type { AsyncVisitor, CollectVisitor, Visitor } from './visitor.ts'
package/src/utils.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { camelCase, isValidVarName } from '@internals/utils'
2
+
3
+ import { narrowSchema } from './guards.ts'
4
+ import type { ParameterNode, SchemaNode } from './nodes/index.ts'
5
+ import type { SchemaType } from './nodes/schema.ts'
6
+
7
+ const plainStringTypes = new Set<SchemaType>(['string', 'uuid', 'email', 'url', 'datetime'] as const)
8
+
9
+ /**
10
+ * Returns `true` when a schema node will be represented as a plain string in generated code.
11
+ *
12
+ * - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings.
13
+ * - `date` and `time` are plain strings when their `representation` is `'string'` rather than `'date'`.
14
+ */
15
+ export function isPlainStringType(node: SchemaNode): boolean {
16
+ if (plainStringTypes.has(node.type)) {
17
+ return true
18
+ }
19
+
20
+ const temporal = narrowSchema(node, 'date') ?? narrowSchema(node, 'time')
21
+ if (temporal) {
22
+ return temporal.representation !== 'date'
23
+ }
24
+
25
+ return false
26
+ }
27
+
28
+ /**
29
+ * Transforms the `name` field of each parameter node according to the given casing strategy.
30
+ *
31
+ * The original `params` array is never mutated — a new array of cloned nodes is returned.
32
+ * When no `casing` is provided the original array is returned as-is.
33
+ *
34
+ * Use this before passing parameters to schema builders so that property keys
35
+ * in the generated output match the desired casing while the original
36
+ * `OperationNode.parameters` array remains untouched for other consumers.
37
+ */
38
+ export function applyParamsCasing(params: Array<ParameterNode>, casing: 'camelcase' | undefined): Array<ParameterNode> {
39
+ if (!casing) {
40
+ return params
41
+ }
42
+
43
+ return params.map((param) => {
44
+ const transformed = casing === 'camelcase' || !isValidVarName(param.name) ? camelCase(param.name) : param.name
45
+
46
+ return { ...param, name: transformed }
47
+ })
48
+ }
package/src/visitor.ts CHANGED
@@ -2,6 +2,10 @@ import type { VisitorDepth } from './constants.ts'
2
2
  import { visitorDepths, WALK_CONCURRENCY } from './constants.ts'
3
3
  import type { Node, OperationNode, ParameterNode, PropertyNode, ResponseNode, RootNode, SchemaNode } from './nodes/index.ts'
4
4
 
5
+ /**
6
+ * Creates a concurrency-limiting wrapper. At most `concurrency` promises may be
7
+ * in-flight simultaneously; additional calls are queued and dispatched as slots free.
8
+ */
5
9
  function createLimit(concurrency: number) {
6
10
  let active = 0
7
11
  const queue: Array<() => void> = []
@@ -81,7 +85,10 @@ export type CollectVisitor<T> = {
81
85
  }
82
86
 
83
87
  /**
84
- * Traversable children of `node`, respecting `recurse` for schema nodes.
88
+ * Returns the immediate traversable children of `node`.
89
+ *
90
+ * For `Schema` nodes, children (properties, items, members) are only included
91
+ * when `recurse` is `true`; shallow traversal omits them entirely.
85
92
  */
86
93
  function getChildren(node: Node, recurse: boolean): Array<Node> {
87
94
  switch (node.kind) {
@@ -106,6 +113,10 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
106
113
  return [node.schema]
107
114
  case 'Response':
108
115
  return node.schema ? [node.schema] : []
116
+ case 'FunctionParameter':
117
+ case 'ObjectBindingParameter':
118
+ case 'FunctionParameters':
119
+ return []
109
120
  }
110
121
  }
111
122
 
@@ -119,6 +130,9 @@ export async function walk(node: Node, visitor: AsyncVisitor, options: VisitorOp
119
130
  return _walk(node, visitor, recurse, limit)
120
131
  }
121
132
 
133
+ /**
134
+ * Internal recursive walk implementation — calls visitor then recurses into children.
135
+ */
122
136
  async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn): Promise<void> {
123
137
  switch (node.kind) {
124
138
  case 'Root':
@@ -139,6 +153,10 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
139
153
  case 'Response':
140
154
  await limit(() => visitor.response?.(node))
141
155
  break
156
+ case 'FunctionParameter':
157
+ case 'ObjectBindingParameter':
158
+ case 'FunctionParameters':
159
+ break
142
160
  }
143
161
 
144
162
  const children = getChildren(node, recurse)
@@ -221,9 +239,13 @@ export function transform(node: Node, visitor: Visitor, options: VisitorOptions
221
239
 
222
240
  return {
223
241
  ...response,
224
- schema: response.schema ? transform(response.schema, visitor, options) : undefined,
242
+ schema: transform(response.schema, visitor, options),
225
243
  }
226
244
  }
245
+ case 'FunctionParameter':
246
+ case 'ObjectBindingParameter':
247
+ case 'FunctionParameters':
248
+ return node
227
249
  }
228
250
  }
229
251
 
@@ -254,6 +276,10 @@ export function collect<T>(node: Node, visitor: CollectVisitor<T>, options: Visi
254
276
  case 'Response':
255
277
  v = visitor.response?.(node)
256
278
  break
279
+ case 'FunctionParameter':
280
+ case 'ObjectBindingParameter':
281
+ case 'FunctionParameters':
282
+ break
257
283
  }
258
284
  if (v !== undefined) results.push(v)
259
285