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

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) {
@@ -97,6 +104,7 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
97
104
  if ('properties' in node && node.properties.length > 0) children.push(...node.properties)
98
105
  if ('items' in node && node.items) children.push(...node.items)
99
106
  if ('members' in node && node.members) children.push(...node.members)
107
+ if ('additionalProperties' in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties)
100
108
 
101
109
  return children
102
110
  }
@@ -106,6 +114,10 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
106
114
  return [node.schema]
107
115
  case 'Response':
108
116
  return node.schema ? [node.schema] : []
117
+ case 'FunctionParameter':
118
+ case 'ObjectBindingParameter':
119
+ case 'FunctionParameters':
120
+ return []
109
121
  }
110
122
  }
111
123
 
@@ -119,6 +131,9 @@ export async function walk(node: Node, visitor: AsyncVisitor, options: VisitorOp
119
131
  return _walk(node, visitor, recurse, limit)
120
132
  }
121
133
 
134
+ /**
135
+ * Internal recursive walk implementation — calls visitor then recurses into children.
136
+ */
122
137
  async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn): Promise<void> {
123
138
  switch (node.kind) {
124
139
  case 'Root':
@@ -139,6 +154,10 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
139
154
  case 'Response':
140
155
  await limit(() => visitor.response?.(node))
141
156
  break
157
+ case 'FunctionParameter':
158
+ case 'ObjectBindingParameter':
159
+ case 'FunctionParameters':
160
+ break
142
161
  }
143
162
 
144
163
  const children = getChildren(node, recurse)
@@ -192,6 +211,9 @@ export function transform(node: Node, visitor: Visitor, options: VisitorOptions
192
211
  ...('properties' in schema && recurse ? { properties: schema.properties.map((p) => transform(p, visitor, options)) } : {}),
193
212
  ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, visitor, options)) } : {}),
194
213
  ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, visitor, options)) } : {}),
214
+ ...('additionalProperties' in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true
215
+ ? { additionalProperties: transform(schema.additionalProperties, visitor, options) }
216
+ : {}),
195
217
  }
196
218
  }
197
219
  case 'Property': {
@@ -221,9 +243,13 @@ export function transform(node: Node, visitor: Visitor, options: VisitorOptions
221
243
 
222
244
  return {
223
245
  ...response,
224
- schema: response.schema ? transform(response.schema, visitor, options) : undefined,
246
+ schema: transform(response.schema, visitor, options),
225
247
  }
226
248
  }
249
+ case 'FunctionParameter':
250
+ case 'ObjectBindingParameter':
251
+ case 'FunctionParameters':
252
+ return node
227
253
  }
228
254
  }
229
255
 
@@ -254,6 +280,10 @@ export function collect<T>(node: Node, visitor: CollectVisitor<T>, options: Visi
254
280
  case 'Response':
255
281
  v = visitor.response?.(node)
256
282
  break
283
+ case 'FunctionParameter':
284
+ case 'ObjectBindingParameter':
285
+ case 'FunctionParameters':
286
+ break
257
287
  }
258
288
  if (v !== undefined) results.push(v)
259
289