@kubb/ast 5.0.0-alpha.2 → 5.0.0-alpha.20

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/visitor.ts CHANGED
@@ -1,7 +1,24 @@
1
1
  import type { VisitorDepth } from './constants.ts'
2
2
  import { visitorDepths, WALK_CONCURRENCY } from './constants.ts'
3
+ import { createParameter, createProperty } from './factory.ts'
3
4
  import type { Node, OperationNode, ParameterNode, PropertyNode, ResponseNode, RootNode, SchemaNode } from './nodes/index.ts'
4
5
 
6
+ /**
7
+ * Creates a small async concurrency limiter.
8
+ *
9
+ * At most `concurrency` tasks are in flight at once. Extra tasks are queued.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const limit = createLimit(2)
14
+ * await Promise.all([
15
+ * limit(() => taskA()),
16
+ * limit(() => taskB()),
17
+ * limit(() => taskC()),
18
+ * ])
19
+ * // only 2 tasks run at the same time
20
+ * ```
21
+ */
5
22
  function createLimit(concurrency: number) {
6
23
  let active = 0
7
24
  const queue: Array<() => void> = []
@@ -31,64 +48,227 @@ function createLimit(concurrency: number) {
31
48
  type LimitFn = ReturnType<typeof createLimit>
32
49
 
33
50
  /**
34
- * Shared options for `walk`, `transform`, and `collect`.
51
+ * Ordered mapping of `[NodeType, ParentType]` pairs.
52
+ *
53
+ * `ParentOf` uses this map to find parent types.
35
54
  */
36
- export type VisitorOptions = {
37
- depth?: VisitorDepth
55
+ type ParentNodeMap = [
56
+ [RootNode, undefined],
57
+ [OperationNode, RootNode],
58
+ [SchemaNode, RootNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode],
59
+ [PropertyNode, SchemaNode],
60
+ [ParameterNode, OperationNode],
61
+ [ResponseNode, OperationNode],
62
+ ]
63
+
64
+ /**
65
+ * Resolves the parent node type for a given AST node type.
66
+ *
67
+ * This is used by visitor context so `ctx.parent` is correctly typed
68
+ * for each callback.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * type RootParent = ParentOf<RootNode>
73
+ * // undefined
74
+ * ```
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * type PropertyParent = ParentOf<PropertyNode>
79
+ * // SchemaNode
80
+ * ```
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * type SchemaParent = ParentOf<SchemaNode>
85
+ * // RootNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode
86
+ * ```
87
+ */
88
+ export type ParentOf<T extends Node, TEntries extends ReadonlyArray<[Node, unknown]> = ParentNodeMap> = TEntries extends [
89
+ infer TEntry extends [Node, unknown],
90
+ ...infer TRest extends ReadonlyArray<[Node, unknown]>,
91
+ ]
92
+ ? T extends TEntry[0]
93
+ ? TEntry[1]
94
+ : ParentOf<T, TRest>
95
+ : Node
96
+
97
+ /**
98
+ * Traversal context passed as the second argument to every visitor callback.
99
+ * `parent` is typed from the current node type.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const visitor: Visitor = {
104
+ * schema(node, { parent }) {
105
+ * // parent type is narrowed by node kind
106
+ * },
107
+ * }
108
+ * ```
109
+ */
110
+ export type VisitorContext<T extends Node = Node> = {
38
111
  /**
39
- * Maximum number of sibling nodes visited concurrently inside `walk`.
40
- * @default 30
112
+ * Parent node of the currently visited node.
113
+ * For `RootNode`, this is `undefined`.
41
114
  */
42
- concurrency?: number
115
+ parent?: ParentOf<T>
43
116
  }
44
117
 
45
118
  /**
46
- * Synchronous visitor for `transform` and `walk`.
119
+ * Synchronous visitor used by `transform`.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const visitor: Visitor = {
124
+ * operation(node) {
125
+ * return { ...node, operationId: `x_${node.operationId}` }
126
+ * },
127
+ * }
128
+ * ```
47
129
  */
48
130
  export type Visitor = {
49
- root?(node: RootNode): void | RootNode
50
- operation?(node: OperationNode): void | OperationNode
51
- schema?(node: SchemaNode): void | SchemaNode
52
- property?(node: PropertyNode): void | PropertyNode
53
- parameter?(node: ParameterNode): void | ParameterNode
54
- response?(node: ResponseNode): void | ResponseNode
131
+ root?(node: RootNode, context: VisitorContext<RootNode>): void | RootNode
132
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): void | OperationNode
133
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): void | SchemaNode
134
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): void | PropertyNode
135
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): void | ParameterNode
136
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): void | ResponseNode
55
137
  }
56
138
 
139
+ /**
140
+ * Utility type for values that can be returned directly or asynchronously.
141
+ */
57
142
  type MaybePromise<T> = T | Promise<T>
58
143
 
59
144
  /**
60
145
  * Async visitor for `walk`. Synchronous `Visitor` objects are compatible.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const visitor: AsyncVisitor = {
150
+ * async operation(node) {
151
+ * await Promise.resolve(node.operationId)
152
+ * },
153
+ * }
154
+ * ```
61
155
  */
62
156
  export type AsyncVisitor = {
63
- root?(node: RootNode): MaybePromise<void | RootNode>
64
- operation?(node: OperationNode): MaybePromise<void | OperationNode>
65
- schema?(node: SchemaNode): MaybePromise<void | SchemaNode>
66
- property?(node: PropertyNode): MaybePromise<void | PropertyNode>
67
- parameter?(node: ParameterNode): MaybePromise<void | ParameterNode>
68
- response?(node: ResponseNode): MaybePromise<void | ResponseNode>
157
+ root?(node: RootNode, context: VisitorContext<RootNode>): MaybePromise<void | RootNode>
158
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): MaybePromise<void | OperationNode>
159
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): MaybePromise<void | SchemaNode>
160
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): MaybePromise<void | PropertyNode>
161
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): MaybePromise<void | ParameterNode>
162
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): MaybePromise<void | ResponseNode>
69
163
  }
70
164
 
71
165
  /**
72
- * Visitor for `collect`.
166
+ * Visitor used by `collect`.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * const visitor: CollectVisitor<string> = {
171
+ * operation(node) {
172
+ * return node.operationId
173
+ * },
174
+ * }
175
+ * ```
73
176
  */
74
177
  export type CollectVisitor<T> = {
75
- root?(node: RootNode): T | undefined
76
- operation?(node: OperationNode): T | undefined
77
- schema?(node: SchemaNode): T | undefined
78
- property?(node: PropertyNode): T | undefined
79
- parameter?(node: ParameterNode): T | undefined
80
- response?(node: ResponseNode): T | undefined
178
+ root?(node: RootNode, context: VisitorContext<RootNode>): T | undefined
179
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): T | undefined
180
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): T | undefined
181
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): T | undefined
182
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): T | undefined
183
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): T | undefined
184
+ }
185
+
186
+ /**
187
+ * Options for `transform`.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * const options: TransformOptions = { depth: 'deep', schema: (node) => node }
192
+ * ```
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * // Only transform the current node, not nested children
197
+ * const options: TransformOptions = { depth: 'shallow', schema: (node) => node }
198
+ * ```
199
+ */
200
+ export type TransformOptions = Visitor & {
201
+ /**
202
+ * Traversal depth (`'deep'` by default).
203
+ * @default 'deep'
204
+ */
205
+ depth?: VisitorDepth
206
+ /**
207
+ * Internal parent override used during recursion.
208
+ */
209
+ parent?: Node
210
+ }
211
+
212
+ /**
213
+ * Options for `walk`.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * const options: WalkOptions = { depth: 'deep', concurrency: 10, root: () => {} }
218
+ * ```
219
+ */
220
+ export type WalkOptions = AsyncVisitor & {
221
+ /**
222
+ * Traversal depth (`'deep'` by default).
223
+ * @default 'deep'
224
+ */
225
+ depth?: VisitorDepth
226
+ /**
227
+ * Maximum number of sibling nodes visited concurrently.
228
+ * @default 30
229
+ */
230
+ concurrency?: number
81
231
  }
82
232
 
83
233
  /**
84
- * Traversable children of `node`, respecting `recurse` for schema nodes.
234
+ * Options for `collect`.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * const options: CollectOptions<string> = { depth: 'shallow', schema: () => undefined }
239
+ * ```
240
+ */
241
+ export type CollectOptions<T> = CollectVisitor<T> & {
242
+ /**
243
+ * Traversal depth (`'deep'` by default).
244
+ * @default 'deep'
245
+ */
246
+ depth?: VisitorDepth
247
+ /**
248
+ * Internal parent override used during recursion.
249
+ */
250
+ parent?: Node
251
+ }
252
+
253
+ /**
254
+ * Returns the immediate traversable children of `node`.
255
+ *
256
+ * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
257
+ * `additionalProperties`) are only included
258
+ * when `recurse` is `true`; shallow mode skips them.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * const children = getChildren(operationNode, true)
263
+ * // returns parameters, requestBody schema (if present), and responses
264
+ * ```
85
265
  */
86
266
  function getChildren(node: Node, recurse: boolean): Array<Node> {
87
267
  switch (node.kind) {
88
268
  case 'Root':
89
269
  return [...node.schemas, ...node.operations]
90
270
  case 'Operation':
91
- return [...node.parameters, ...(node.requestBody ? [node.requestBody] : []), ...node.responses]
271
+ return [...node.parameters, ...(node.requestBody?.schema ? [node.requestBody.schema] : []), ...node.responses]
92
272
  case 'Schema': {
93
273
  const children: Array<Node> = []
94
274
 
@@ -97,6 +277,7 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
97
277
  if ('properties' in node && node.properties.length > 0) children.push(...node.properties)
98
278
  if ('items' in node && node.items) children.push(...node.items)
99
279
  if ('members' in node && node.members) children.push(...node.members)
280
+ if ('additionalProperties' in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties)
100
281
 
101
282
  return children
102
283
  }
@@ -106,159 +287,273 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
106
287
  return [node.schema]
107
288
  case 'Response':
108
289
  return node.schema ? [node.schema] : []
290
+ case 'FunctionParameter':
291
+ case 'ObjectBindingParameter':
292
+ case 'FunctionParameters':
293
+ return []
109
294
  }
110
295
  }
111
296
 
112
297
  /**
113
298
  * Depth-first traversal for side effects. Visitor return values are ignored.
114
- * Sibling nodes at each level are visited concurrently up to `options.concurrency` (default: 30).
299
+ * Sibling nodes at each level are visited concurrently up to `options.concurrency`
300
+ * (default: `WALK_CONCURRENCY`).
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * await walk(root, {
305
+ * operation(node) {
306
+ * console.log(node.operationId)
307
+ * },
308
+ * })
309
+ * ```
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * // Visit only the current node
314
+ * await walk(root, { depth: 'shallow', root: () => {} })
315
+ * ```
115
316
  */
116
- export async function walk(node: Node, visitor: AsyncVisitor, options: VisitorOptions = {}): Promise<void> {
317
+ export async function walk(node: Node, options: WalkOptions): Promise<void> {
117
318
  const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
118
319
  const limit = createLimit(options.concurrency ?? WALK_CONCURRENCY)
119
- return _walk(node, visitor, recurse, limit)
320
+
321
+ return _walk(node, options, recurse, limit, undefined)
120
322
  }
121
323
 
122
- async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn): Promise<void> {
324
+ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn, parent: Node | undefined): Promise<void> {
123
325
  switch (node.kind) {
124
326
  case 'Root':
125
- await limit(() => visitor.root?.(node))
327
+ await limit(() => visitor.root?.(node, { parent: parent as ParentOf<RootNode> }))
126
328
  break
127
329
  case 'Operation':
128
- await limit(() => visitor.operation?.(node))
330
+ await limit(() => visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> }))
129
331
  break
130
332
  case 'Schema':
131
- await limit(() => visitor.schema?.(node))
333
+ await limit(() => visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> }))
132
334
  break
133
335
  case 'Property':
134
- await limit(() => visitor.property?.(node))
336
+ await limit(() => visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> }))
135
337
  break
136
338
  case 'Parameter':
137
- await limit(() => visitor.parameter?.(node))
339
+ await limit(() => visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> }))
138
340
  break
139
341
  case 'Response':
140
- await limit(() => visitor.response?.(node))
342
+ await limit(() => visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> }))
343
+ break
344
+ case 'FunctionParameter':
345
+ case 'ObjectBindingParameter':
346
+ case 'FunctionParameters':
141
347
  break
142
348
  }
143
349
 
144
350
  const children = getChildren(node, recurse)
145
- await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit)))
351
+ await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)))
146
352
  }
147
353
 
148
354
  /**
149
- * Depth-first immutable transformation. Visitor return values replace nodes; `undefined` keeps the original.
355
+ * Runs a depth-first immutable transform.
356
+ *
357
+ * If a visitor returns a node, it replaces the current node.
358
+ * If it returns `undefined`, the current node stays the same.
359
+ *
360
+ * @example
361
+ * ```ts
362
+ * const next = transform(root, {
363
+ * operation(node) {
364
+ * return { ...node, operationId: `prefixed_${node.operationId}` }
365
+ * },
366
+ * })
367
+ * ```
368
+ *
369
+ * @example
370
+ * ```ts
371
+ * // Shallow mode: only transform the input node
372
+ * const next = transform(root, { depth: 'shallow', root: (node) => node })
373
+ * ```
150
374
  */
151
- export function transform(node: RootNode, visitor: Visitor, options?: VisitorOptions): RootNode
152
- export function transform(node: OperationNode, visitor: Visitor, options?: VisitorOptions): OperationNode
153
- export function transform(node: SchemaNode, visitor: Visitor, options?: VisitorOptions): SchemaNode
154
- export function transform(node: PropertyNode, visitor: Visitor, options?: VisitorOptions): PropertyNode
155
- export function transform(node: ParameterNode, visitor: Visitor, options?: VisitorOptions): ParameterNode
156
- export function transform(node: ResponseNode, visitor: Visitor, options?: VisitorOptions): ResponseNode
157
- export function transform(node: Node, visitor: Visitor, options?: VisitorOptions): Node
158
- export function transform(node: Node, visitor: Visitor, options: VisitorOptions = {}): Node {
159
- const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
375
+ export function transform(node: RootNode, options: TransformOptions): RootNode
376
+ export function transform(node: OperationNode, options: TransformOptions): OperationNode
377
+ export function transform(node: SchemaNode, options: TransformOptions): SchemaNode
378
+ export function transform(node: PropertyNode, options: TransformOptions): PropertyNode
379
+ export function transform(node: ParameterNode, options: TransformOptions): ParameterNode
380
+ export function transform(node: ResponseNode, options: TransformOptions): ResponseNode
381
+ export function transform(node: Node, options: TransformOptions): Node
382
+ export function transform(node: Node, options: TransformOptions): Node {
383
+ const { depth, parent, ...visitor } = options
384
+ const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
160
385
 
161
386
  switch (node.kind) {
162
387
  case 'Root': {
163
388
  let root = node
164
- const replaced = visitor.root?.(root)
389
+ const replaced = visitor.root?.(root, { parent: parent as ParentOf<RootNode> })
165
390
  if (replaced) root = replaced
166
391
 
167
392
  return {
168
393
  ...root,
169
- schemas: root.schemas.map((s) => transform(s, visitor, options)),
170
- operations: root.operations.map((op) => transform(op, visitor, options)),
394
+ schemas: root.schemas.map((s) => transform(s, { ...options, parent: root })),
395
+ operations: root.operations.map((op) => transform(op, { ...options, parent: root })),
171
396
  }
172
397
  }
173
398
  case 'Operation': {
174
399
  let op = node
175
- const replaced = visitor.operation?.(op)
400
+ const replaced = visitor.operation?.(op, { parent: parent as ParentOf<OperationNode> })
176
401
  if (replaced) op = replaced
177
402
 
178
403
  return {
179
404
  ...op,
180
- parameters: op.parameters.map((p) => transform(p, visitor, options)),
181
- requestBody: op.requestBody ? transform(op.requestBody, visitor, options) : undefined,
182
- responses: op.responses.map((r) => transform(r, visitor, options)),
405
+ parameters: op.parameters.map((p) => transform(p, { ...options, parent: op })),
406
+ requestBody: op.requestBody
407
+ ? { ...op.requestBody, schema: op.requestBody.schema ? transform(op.requestBody.schema, { ...options, parent: op }) : undefined }
408
+ : undefined,
409
+ responses: op.responses.map((r) => transform(r, { ...options, parent: op })),
183
410
  }
184
411
  }
185
412
  case 'Schema': {
186
413
  let schema = node
187
- const replaced = visitor.schema?.(schema)
414
+ const replaced = visitor.schema?.(schema, { parent: parent as ParentOf<SchemaNode> })
188
415
  if (replaced) schema = replaced
189
416
 
417
+ const childOptions = { ...options, parent: schema }
418
+
190
419
  return {
191
420
  ...schema,
192
- ...('properties' in schema && recurse ? { properties: schema.properties.map((p) => transform(p, visitor, options)) } : {}),
193
- ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, visitor, options)) } : {}),
194
- ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, visitor, options)) } : {}),
195
- }
421
+ ...('properties' in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {}),
422
+ ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {}),
423
+ ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {}),
424
+ ...('additionalProperties' in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true
425
+ ? { additionalProperties: transform(schema.additionalProperties, childOptions) }
426
+ : {}),
427
+ } as SchemaNode
196
428
  }
197
429
  case 'Property': {
198
430
  let prop = node
199
- const replaced = visitor.property?.(prop)
431
+ const replaced = visitor.property?.(prop, { parent: parent as ParentOf<PropertyNode> })
200
432
  if (replaced) prop = replaced
201
433
 
202
- return {
434
+ return createProperty({
203
435
  ...prop,
204
- schema: transform(prop.schema, visitor, options),
205
- }
436
+ schema: transform(prop.schema, { ...options, parent: prop }),
437
+ })
206
438
  }
207
439
  case 'Parameter': {
208
440
  let param = node
209
- const replaced = visitor.parameter?.(param)
441
+ const replaced = visitor.parameter?.(param, { parent: parent as ParentOf<ParameterNode> })
210
442
  if (replaced) param = replaced
211
443
 
212
- return {
444
+ return createParameter({
213
445
  ...param,
214
- schema: transform(param.schema, visitor, options),
215
- }
446
+ schema: transform(param.schema, { ...options, parent: param }),
447
+ })
216
448
  }
217
449
  case 'Response': {
218
450
  let response = node
219
- const replaced = visitor.response?.(response)
451
+ const replaced = visitor.response?.(response, { parent: parent as ParentOf<ResponseNode> })
220
452
  if (replaced) response = replaced
221
453
 
222
454
  return {
223
455
  ...response,
224
- schema: response.schema ? transform(response.schema, visitor, options) : undefined,
456
+ schema: transform(response.schema, { ...options, parent: response }),
225
457
  }
226
458
  }
459
+ case 'FunctionParameter':
460
+ case 'ObjectBindingParameter':
461
+ case 'FunctionParameters':
462
+ return node
227
463
  }
228
464
  }
229
465
 
230
466
  /**
231
- * Depth-first synchronous reduction. Collects non-`undefined` visitor return values into an array.
467
+ * Composes multiple visitors into one visitor, applied left to right.
468
+ *
469
+ * For each node kind, output from one visitor is input to the next.
470
+ * If a visitor returns `undefined`, the previous node value is kept.
471
+ *
472
+ * @example
473
+ * ```ts
474
+ * const visitor = composeTransformers(
475
+ * { operation: (node) => ({ ...node, operationId: `a_${node.operationId}` }) },
476
+ * { operation: (node) => ({ ...node, operationId: `b_${node.operationId}` }) },
477
+ * )
478
+ * ```
232
479
  */
233
- export function collect<T>(node: Node, visitor: CollectVisitor<T>, options: VisitorOptions = {}): Array<T> {
234
- const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
480
+ export function composeTransformers(...visitors: Array<Visitor>): Visitor {
481
+ return {
482
+ root(node, context) {
483
+ return visitors.reduce<RootNode>((acc, v) => v.root?.(acc, context) ?? acc, node)
484
+ },
485
+ operation(node, context) {
486
+ return visitors.reduce<OperationNode>((acc, v) => v.operation?.(acc, context) ?? acc, node)
487
+ },
488
+ schema(node, context) {
489
+ return visitors.reduce<SchemaNode>((acc, v) => v.schema?.(acc, context) ?? acc, node)
490
+ },
491
+ property(node, context) {
492
+ return visitors.reduce<PropertyNode>((acc, v) => v.property?.(acc, context) ?? acc, node)
493
+ },
494
+ parameter(node, context) {
495
+ return visitors.reduce<ParameterNode>((acc, v) => v.parameter?.(acc, context) ?? acc, node)
496
+ },
497
+ response(node, context) {
498
+ return visitors.reduce<ResponseNode>((acc, v) => v.response?.(acc, context) ?? acc, node)
499
+ },
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Runs a depth-first synchronous collection pass.
505
+ *
506
+ * Non-`undefined` values returned by visitor callbacks are appended to the result.
507
+ *
508
+ * @example
509
+ * ```ts
510
+ * const ids = collect(root, {
511
+ * operation(node) {
512
+ * return node.operationId
513
+ * },
514
+ * })
515
+ * ```
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * // Collect from only the current node
520
+ * const values = collect(root, { depth: 'shallow', root: () => 'root' })
521
+ * ```
522
+ */
523
+ export function collect<T>(node: Node, options: CollectOptions<T>): Array<T> {
524
+ const { depth, parent, ...visitor } = options
525
+ const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
235
526
  const results: Array<T> = []
236
527
 
237
528
  let v: T | undefined
238
529
  switch (node.kind) {
239
530
  case 'Root':
240
- v = visitor.root?.(node)
531
+ v = visitor.root?.(node, { parent: parent as ParentOf<RootNode> })
241
532
  break
242
533
  case 'Operation':
243
- v = visitor.operation?.(node)
534
+ v = visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> })
244
535
  break
245
536
  case 'Schema':
246
- v = visitor.schema?.(node)
537
+ v = visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> })
247
538
  break
248
539
  case 'Property':
249
- v = visitor.property?.(node)
540
+ v = visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> })
250
541
  break
251
542
  case 'Parameter':
252
- v = visitor.parameter?.(node)
543
+ v = visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> })
253
544
  break
254
545
  case 'Response':
255
- v = visitor.response?.(node)
546
+ v = visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> })
547
+ break
548
+ case 'FunctionParameter':
549
+ case 'ObjectBindingParameter':
550
+ case 'FunctionParameters':
256
551
  break
257
552
  }
258
553
  if (v !== undefined) results.push(v)
259
554
 
260
555
  for (const child of getChildren(node, recurse)) {
261
- for (const item of collect(child, visitor, options)) {
556
+ for (const item of collect(child, { ...options, parent: node })) {
262
557
  results.push(item)
263
558
  }
264
559
  }