@kubb/ast 5.0.0-beta.29 → 5.0.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -224,6 +224,60 @@ const mediaTypes = {
224
224
  videoMp4: "video/mp4"
225
225
  };
226
226
  //#endregion
227
+ //#region src/dialect.ts
228
+ /**
229
+ * Identity helper that types a {@link SchemaDialect} for an adapter. Like
230
+ * `defineParser`, it adds no runtime behavior — it pins the dialect's type for
231
+ * inference and gives adapter authors a discoverable anchor.
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * export const oasDialect = defineSchemaDialect({
236
+ * name: 'oas',
237
+ * isNullable,
238
+ * isReference,
239
+ * isDiscriminator,
240
+ * isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
241
+ * resolveRef,
242
+ * })
243
+ * ```
244
+ */
245
+ function defineSchemaDialect(dialect) {
246
+ return dialect;
247
+ }
248
+ //#endregion
249
+ //#region src/dispatch.ts
250
+ /**
251
+ * Walks an ordered list of {@link DispatchRule}s and returns the first node produced.
252
+ *
253
+ * This is the shared backbone for spec adapters (OpenAPI today, AsyncAPI and others later).
254
+ * The contract an adapter follows is intentionally minimal:
255
+ *
256
+ * context → [rule.match → rule.convert] → node
257
+ *
258
+ * An adapter derives a context from a source spec node, then declares an ordered table of
259
+ * rules mapping spec shapes onto Kubb AST nodes. To add support for a new spec, write a new
260
+ * context type and a new rules table — the traversal here is reused unchanged.
261
+ *
262
+ * Order is significant: earlier rules win, so list higher-precedence or more specific shapes
263
+ * first (e.g. composition keywords before plain `type`). A rule whose `match` returns `true`
264
+ * may still `convert` to `null` to defer to later rules. When no rule produces a node this
265
+ * returns `null`, leaving the caller to apply its own fallback.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * const node = dispatch(schemaRules, schemaContext) ?? createSchema({ type: fallbackType })
270
+ * ```
271
+ */
272
+ function dispatch(rules, context) {
273
+ for (const rule of rules) {
274
+ if (!rule.match(context)) continue;
275
+ const node = rule.convert(context);
276
+ if (node !== null && node !== void 0) return node;
277
+ }
278
+ return null;
279
+ }
280
+ //#endregion
227
281
  //#region ../../internals/utils/src/casing.ts
228
282
  /**
229
283
  * Shared implementation for camelCase and PascalCase conversion.
@@ -498,6 +552,19 @@ const isOutputNode = isKind("Output");
498
552
  */
499
553
  const isOperationNode = isKind("Operation");
500
554
  /**
555
+ * Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * if (isHttpOperationNode(node)) {
560
+ * console.log(node.method, node.path)
561
+ * }
562
+ * ```
563
+ */
564
+ function isHttpOperationNode(node) {
565
+ return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
566
+ }
567
+ /**
501
568
  * Returns `true` when the input is a `SchemaNode`.
502
569
  *
503
570
  * @example
@@ -560,58 +627,84 @@ function createLimit(concurrency) {
560
627
  });
561
628
  };
562
629
  }
630
+ const visitorKeysByKind = {
631
+ Input: ["schemas", "operations"],
632
+ Operation: [
633
+ "parameters",
634
+ "requestBody",
635
+ "responses"
636
+ ],
637
+ RequestBody: ["content"],
638
+ Content: ["schema"],
639
+ Response: ["content"],
640
+ Schema: [
641
+ "properties",
642
+ "items",
643
+ "members",
644
+ "additionalProperties"
645
+ ],
646
+ Property: ["schema"],
647
+ Parameter: ["schema"]
648
+ };
563
649
  /**
564
- * Returns the immediate traversable children of `node`.
650
+ * Returns `true` when `value` is an AST node (an object carrying a `kind`).
651
+ */
652
+ function isNode(value) {
653
+ return typeof value === "object" && value !== null && "kind" in value;
654
+ }
655
+ /**
656
+ * Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
565
657
  *
566
- * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
567
- * `additionalProperties`) are only included
568
- * when `recurse` is `true`; shallow mode skips them.
658
+ * `Schema` children are only included when `recurse` is `true`; shallow mode skips them.
569
659
  *
570
660
  * @example
571
661
  * ```ts
572
662
  * const children = getChildren(operationNode, true)
573
- * // returns parameters, requestBody schema (if present), and responses
663
+ * // returns parameters, the request body, and responses
574
664
  * ```
575
665
  */
576
666
  function* getChildren(node, recurse) {
577
- if (node.kind === "Input") {
578
- yield* node.schemas;
579
- yield* node.operations;
580
- return;
581
- }
582
- if (node.kind === "Output") return;
583
- if (node.kind === "Operation") {
584
- yield* node.parameters;
585
- if (node.requestBody?.content) {
586
- for (const c of node.requestBody.content) if (c.schema) yield c.schema;
587
- }
588
- yield* node.responses;
589
- return;
590
- }
591
- if (node.kind === "Schema") {
592
- if (!recurse) return;
593
- if ("properties" in node && node.properties.length > 0) yield* node.properties;
594
- if ("items" in node && node.items) yield* node.items;
595
- if ("members" in node && node.members) yield* node.members;
596
- if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) yield node.additionalProperties;
597
- return;
598
- }
599
- if (node.kind === "Property") {
600
- yield node.schema;
601
- return;
602
- }
603
- if (node.kind === "Parameter") {
604
- yield node.schema;
605
- return;
606
- }
607
- if (node.kind === "Response") {
608
- if (node.content) {
609
- for (const c of node.content) if (c.schema) yield c.schema;
610
- }
611
- return;
667
+ if (node.kind === "Schema" && !recurse) return;
668
+ const keys = visitorKeysByKind[node.kind];
669
+ if (!keys) return;
670
+ const record = node;
671
+ for (const key of keys) {
672
+ const value = record[key];
673
+ if (Array.isArray(value)) {
674
+ for (const item of value) if (isNode(item)) yield item;
675
+ } else if (isNode(value)) yield value;
612
676
  }
613
677
  }
614
678
  /**
679
+ * Maps a node `kind` to the matching visitor callback name. Only the seven
680
+ * traversable node kinds have an entry; every other kind resolves to
681
+ * `undefined` and is skipped.
682
+ */
683
+ const VISITOR_KEY_BY_KIND = {
684
+ Input: "input",
685
+ Output: "output",
686
+ Operation: "operation",
687
+ Schema: "schema",
688
+ Property: "property",
689
+ Parameter: "parameter",
690
+ Response: "response"
691
+ };
692
+ /**
693
+ * Invokes the visitor callback that matches `node.kind`, passing the traversal
694
+ * context. Returns the callback's result (a replacement node, a collected
695
+ * value, or `undefined` when no callback is registered for the kind).
696
+ *
697
+ * Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
698
+ * in one place. `TResult` is the caller's expected return: the same node type
699
+ * for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
700
+ */
701
+ function applyVisitor(node, visitor, parent) {
702
+ const key = VISITOR_KEY_BY_KIND[node.kind];
703
+ if (!key) return void 0;
704
+ const fn = visitor[key];
705
+ return fn?.(node, { parent });
706
+ }
707
+ /**
615
708
  * Async depth-first traversal for side effects. Visitor return values are
616
709
  * ignored. Use `transform` when you want to rewrite nodes.
617
710
  *
@@ -637,122 +730,63 @@ async function walk(node, options) {
637
730
  return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
638
731
  }
639
732
  async function _walk(node, visitor, recurse, limit, parent) {
640
- switch (node.kind) {
641
- case "Input":
642
- await limit(() => visitor.input?.(node, { parent }));
643
- break;
644
- case "Output":
645
- await limit(() => visitor.output?.(node, { parent }));
646
- break;
647
- case "Operation":
648
- await limit(() => visitor.operation?.(node, { parent }));
649
- break;
650
- case "Schema":
651
- await limit(() => visitor.schema?.(node, { parent }));
652
- break;
653
- case "Property":
654
- await limit(() => visitor.property?.(node, { parent }));
655
- break;
656
- case "Parameter":
657
- await limit(() => visitor.parameter?.(node, { parent }));
658
- break;
659
- case "Response":
660
- await limit(() => visitor.response?.(node, { parent }));
661
- break;
662
- }
733
+ await limit(() => applyVisitor(node, visitor, parent));
663
734
  const children = getChildren(node, recurse);
664
735
  for (const child of children) await _walk(child, visitor, recurse, limit, node);
665
736
  }
666
737
  function transform(node, options) {
667
738
  const { depth, parent, ...visitor } = options;
668
739
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
669
- if (node.kind === "Input") {
670
- const input = visitor.input?.(node, { parent }) ?? node;
671
- return {
672
- ...input,
673
- schemas: input.schemas.map((s) => transform(s, {
674
- ...options,
675
- parent: input
676
- })),
677
- operations: input.operations.map((op) => transform(op, {
678
- ...options,
679
- parent: input
680
- }))
681
- };
682
- }
683
- if (node.kind === "Output") return visitor.output?.(node, { parent }) ?? node;
684
- if (node.kind === "Operation") {
685
- const op = visitor.operation?.(node, { parent }) ?? node;
686
- return {
687
- ...op,
688
- parameters: op.parameters.map((p) => transform(p, {
689
- ...options,
690
- parent: op
691
- })),
692
- requestBody: op.requestBody ? {
693
- ...op.requestBody,
694
- content: op.requestBody.content?.map((c) => ({
695
- ...c,
696
- schema: c.schema ? transform(c.schema, {
697
- ...options,
698
- parent: op
699
- }) : void 0
700
- }))
701
- } : void 0,
702
- responses: op.responses.map((r) => transform(r, {
703
- ...options,
704
- parent: op
705
- }))
706
- };
707
- }
708
- if (node.kind === "Schema") {
709
- const schema = visitor.schema?.(node, { parent }) ?? node;
710
- const childOptions = {
711
- ...options,
712
- parent: schema
713
- };
714
- return {
715
- ...schema,
716
- ..."properties" in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {},
717
- ..."items" in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {},
718
- ..."members" in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {},
719
- ..."additionalProperties" in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true ? { additionalProperties: transform(schema.additionalProperties, childOptions) } : {}
720
- };
721
- }
722
- if (node.kind === "Property") {
723
- const prop = visitor.property?.(node, { parent }) ?? node;
724
- return createProperty({
725
- ...prop,
726
- schema: transform(prop.schema, {
727
- ...options,
728
- parent: prop
729
- })
730
- });
731
- }
732
- if (node.kind === "Parameter") {
733
- const param = visitor.parameter?.(node, { parent }) ?? node;
734
- return createParameter({
735
- ...param,
736
- schema: transform(param.schema, {
737
- ...options,
738
- parent: param
739
- })
740
- });
741
- }
742
- if (node.kind === "Response") {
743
- const response = visitor.response?.(node, { parent }) ?? node;
744
- return {
745
- ...response,
746
- content: response.content?.map((entry) => ({
747
- ...entry,
748
- schema: entry.schema ? transform(entry.schema, {
749
- ...options,
750
- parent: response
751
- }) : entry.schema
752
- }))
753
- };
740
+ const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
741
+ if (rebuilt === node) return node;
742
+ const finalize = nodeFinalizers[rebuilt.kind];
743
+ return finalize ? finalize(rebuilt) : rebuilt;
744
+ }
745
+ /**
746
+ * Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
747
+ * resync schema optionality against their `required` flag once the schema may
748
+ * have changed.
749
+ */
750
+ const nodeFinalizers = {
751
+ Property: (node) => createProperty(node),
752
+ Parameter: (node) => createParameter(node)
753
+ };
754
+ /**
755
+ * Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
756
+ * each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
757
+ * `Schema` children are skipped in shallow mode.
758
+ */
759
+ function transformChildren(node, options, recurse) {
760
+ if (node.kind === "Schema" && !recurse) return node;
761
+ const keys = visitorKeysByKind[node.kind];
762
+ if (!keys) return node;
763
+ const record = node;
764
+ const childOptions = {
765
+ ...options,
766
+ parent: node
767
+ };
768
+ let updates;
769
+ for (const key of keys) {
770
+ if (!(key in record)) continue;
771
+ const value = record[key];
772
+ if (Array.isArray(value)) {
773
+ let changed = false;
774
+ const mapped = value.map((item) => {
775
+ if (!isNode(item)) return item;
776
+ const next = transform(item, childOptions);
777
+ if (next !== item) changed = true;
778
+ return next;
779
+ });
780
+ if (changed) (updates ??= {})[key] = mapped;
781
+ } else if (isNode(value)) {
782
+ const next = transform(value, childOptions);
783
+ if (next !== value) (updates ??= {})[key] = next;
784
+ }
754
785
  }
755
- return node;
786
+ return updates ? {
787
+ ...node,
788
+ ...updates
789
+ } : node;
756
790
  }
757
791
  /**
758
792
  * Lazy depth-first collection pass. Yields every non-null value returned by
@@ -773,30 +807,7 @@ function transform(node, options) {
773
807
  function* collectLazy(node, options) {
774
808
  const { depth, parent, ...visitor } = options;
775
809
  const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
776
- let v;
777
- switch (node.kind) {
778
- case "Input":
779
- v = visitor.input?.(node, { parent });
780
- break;
781
- case "Output":
782
- v = visitor.output?.(node, { parent });
783
- break;
784
- case "Operation":
785
- v = visitor.operation?.(node, { parent });
786
- break;
787
- case "Schema":
788
- v = visitor.schema?.(node, { parent });
789
- break;
790
- case "Property":
791
- v = visitor.property?.(node, { parent });
792
- break;
793
- case "Parameter":
794
- v = visitor.parameter?.(node, { parent });
795
- break;
796
- case "Response":
797
- v = visitor.response?.(node, { parent });
798
- break;
799
- }
810
+ const v = applyVisitor(node, visitor, parent);
800
811
  if (v != null) yield v;
801
812
  for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
802
813
  ...options,
@@ -1158,6 +1169,16 @@ function combineSources(sources) {
1158
1169
  return [...seen.values()];
1159
1170
  }
1160
1171
  /**
1172
+ * Merges `incoming` names into `existing`, preserving order and dropping duplicates.
1173
+ *
1174
+ * Shared by `combineExports` and `combineImports` for the same-path name-merge case.
1175
+ */
1176
+ function mergeNameArrays(existing, incoming) {
1177
+ const merged = new Set(existing);
1178
+ for (const name of incoming) merged.add(name);
1179
+ return [...merged];
1180
+ }
1181
+ /**
1161
1182
  * Deduplicates and merges `ExportNode` objects by path and type.
1162
1183
  *
1163
1184
  * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
@@ -1178,11 +1199,8 @@ function combineExports(exports) {
1178
1199
  if (!name.length) continue;
1179
1200
  const key = pathTypeKey(path, isTypeOnly);
1180
1201
  const existing = namedByPath.get(key);
1181
- if (existing && Array.isArray(existing.name)) {
1182
- const merged = new Set(existing.name);
1183
- for (const n of name) merged.add(n);
1184
- existing.name = [...merged];
1185
- } else {
1202
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
1203
+ else {
1186
1204
  const newItem = {
1187
1205
  ...curr,
1188
1206
  name: [...new Set(name)]
@@ -1240,11 +1258,8 @@ function combineImports(imports, exports, source) {
1240
1258
  if (!name.length) continue;
1241
1259
  const key = pathTypeKey(path, isTypeOnly);
1242
1260
  const existing = namedByPath.get(key);
1243
- if (existing && Array.isArray(existing.name)) {
1244
- const merged = new Set(existing.name);
1245
- for (const n of name) merged.add(n);
1246
- existing.name = [...merged];
1247
- } else {
1261
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
1262
+ else {
1248
1263
  const newItem = {
1249
1264
  ...curr,
1250
1265
  name
@@ -1463,6 +1478,29 @@ function syncOptionality(schema, required) {
1463
1478
  };
1464
1479
  }
1465
1480
  /**
1481
+ * Identity-preserving node update: returns `node` unchanged when every field in
1482
+ * `changes` already equals (by reference) the current value, otherwise a new node
1483
+ * with the changes applied.
1484
+ *
1485
+ * Mirrors the TypeScript compiler's `factory.updateX` contract — pair it with the
1486
+ * structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
1487
+ * downstream passes can detect "nothing changed" by identity. Comparison is
1488
+ * shallow: a structurally-equal but newly-allocated array/object counts as a change.
1489
+ *
1490
+ * @example
1491
+ * ```ts
1492
+ * update(node, { name: node.name }) // -> same `node` reference
1493
+ * update(node, { name: 'renamed' }) // -> new node, `name` replaced
1494
+ * ```
1495
+ */
1496
+ function update(node, changes) {
1497
+ for (const key in changes) if (changes[key] !== node[key]) return {
1498
+ ...node,
1499
+ ...changes
1500
+ };
1501
+ return node;
1502
+ }
1503
+ /**
1466
1504
  * Creates an `InputNode` with stable defaults for `schemas` and `operations`.
1467
1505
  *
1468
1506
  * @example
@@ -1527,35 +1565,35 @@ function createOutput(overrides = {}) {
1527
1565
  };
1528
1566
  }
1529
1567
  /**
1530
- * Creates an `OperationNode` with default empty arrays for `tags`, `parameters`, and `responses`.
1531
- *
1532
- * @example
1533
- * ```ts
1534
- * const operation = createOperation({
1535
- * operationId: 'getPetById',
1536
- * method: 'GET',
1537
- * path: '/pet/{petId}',
1538
- * })
1539
- * // tags, parameters, and responses are []
1540
- * ```
1541
- *
1542
- * @example
1543
- * ```ts
1544
- * const operation = createOperation({
1545
- * operationId: 'findPets',
1546
- * method: 'GET',
1547
- * path: '/pet/findByStatus',
1548
- * tags: ['pet'],
1549
- * })
1550
- * ```
1568
+ * Creates a `ContentNode` for a single request-body or response content type.
1569
+ */
1570
+ function createContent(props) {
1571
+ return {
1572
+ ...props,
1573
+ kind: "Content"
1574
+ };
1575
+ }
1576
+ /**
1577
+ * Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
1551
1578
  */
1579
+ function createRequestBody(props) {
1580
+ return {
1581
+ ...props,
1582
+ kind: "RequestBody",
1583
+ content: props.content?.map(createContent)
1584
+ };
1585
+ }
1552
1586
  function createOperation(props) {
1587
+ const { requestBody, ...rest } = props;
1588
+ const isHttp = rest.method !== void 0 && rest.path !== void 0;
1553
1589
  return {
1554
1590
  tags: [],
1555
1591
  parameters: [],
1556
1592
  responses: [],
1557
- ...props,
1558
- kind: "Operation"
1593
+ ...rest,
1594
+ ...isHttp ? { protocol: "http" } : {},
1595
+ kind: "Operation",
1596
+ requestBody: requestBody ? createRequestBody(requestBody) : void 0
1559
1597
  };
1560
1598
  }
1561
1599
  /**
@@ -1683,14 +1721,15 @@ function createParameter(props) {
1683
1721
  */
1684
1722
  function createResponse(props) {
1685
1723
  const { schema, mediaType, keysToOmit, content, ...rest } = props;
1724
+ const entries = content ?? (schema ? [{
1725
+ contentType: mediaType ?? "application/json",
1726
+ schema,
1727
+ keysToOmit: keysToOmit ?? null
1728
+ }] : void 0);
1686
1729
  return {
1687
1730
  ...rest,
1688
1731
  kind: "Response",
1689
- content: content ?? (schema ? [{
1690
- contentType: mediaType ?? "application/json",
1691
- schema,
1692
- keysToOmit: keysToOmit ?? null
1693
- }] : void 0)
1732
+ content: entries?.map(createContent)
1694
1733
  };
1695
1734
  }
1696
1735
  /**
@@ -2326,6 +2365,7 @@ exports.containsCircularRef = containsCircularRef;
2326
2365
  exports.createArrowFunction = createArrowFunction;
2327
2366
  exports.createBreak = createBreak;
2328
2367
  exports.createConst = createConst;
2368
+ exports.createContent = createContent;
2329
2369
  exports.createDiscriminantNode = createDiscriminantNode;
2330
2370
  exports.createExport = createExport;
2331
2371
  exports.createFile = createFile;
@@ -2343,6 +2383,7 @@ exports.createParameterGroup = createParameterGroup;
2343
2383
  exports.createParamsType = createParamsType;
2344
2384
  exports.createPrinterFactory = createPrinterFactory;
2345
2385
  exports.createProperty = createProperty;
2386
+ exports.createRequestBody = createRequestBody;
2346
2387
  exports.createResponse = createResponse;
2347
2388
  exports.createSchema = createSchema;
2348
2389
  exports.createSource = createSource;
@@ -2350,12 +2391,15 @@ exports.createStreamInput = createStreamInput;
2350
2391
  exports.createText = createText;
2351
2392
  exports.createType = createType;
2352
2393
  exports.definePrinter = definePrinter;
2394
+ exports.defineSchemaDialect = defineSchemaDialect;
2395
+ exports.dispatch = dispatch;
2353
2396
  exports.enumPropName = enumPropName;
2354
2397
  exports.extractRefName = extractRefName;
2355
2398
  exports.extractStringsFromNodes = extractStringsFromNodes;
2356
2399
  exports.findCircularSchemas = findCircularSchemas;
2357
2400
  exports.findDiscriminator = findDiscriminator;
2358
2401
  exports.httpMethods = httpMethods;
2402
+ exports.isHttpOperationNode = isHttpOperationNode;
2359
2403
  exports.isInputNode = isInputNode;
2360
2404
  exports.isOperationNode = isOperationNode;
2361
2405
  exports.isOutputNode = isOutputNode;
@@ -2375,6 +2419,7 @@ exports.simplifyUnion = simplifyUnion;
2375
2419
  exports.syncOptionality = syncOptionality;
2376
2420
  exports.syncSchemaRef = syncSchemaRef;
2377
2421
  exports.transform = transform;
2422
+ exports.update = update;
2378
2423
  exports.walk = walk;
2379
2424
 
2380
2425
  //# sourceMappingURL=index.cjs.map