@kubb/adapter-oas 5.0.0-beta.54 → 5.0.0-beta.56

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
@@ -22,19 +22,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
  //#endregion
24
24
  let _kubb_core = require("@kubb/core");
25
- let oas = require("oas");
26
- oas = __toESM(oas, 1);
27
25
  let node_path = require("node:path");
28
26
  node_path = __toESM(node_path, 1);
29
27
  let node_fs_promises = require("node:fs/promises");
30
- let oas_normalize = require("oas-normalize");
31
- oas_normalize = __toESM(oas_normalize, 1);
28
+ let _readme_openapi_parser = require("@readme/openapi-parser");
29
+ let yaml = require("yaml");
32
30
  let api_ref_bundler = require("api-ref-bundler");
33
- let js_yaml = require("js-yaml");
34
- js_yaml = __toESM(js_yaml, 1);
35
31
  let _kubb_ast_utils = require("@kubb/ast/utils");
36
- let oas_types = require("oas/types");
37
- let oas_utils = require("oas/utils");
32
+ let _kubb_ast = require("@kubb/ast");
38
33
  //#region src/constants.ts
39
34
  /**
40
35
  * Default parser options applied when no explicit options are provided.
@@ -66,6 +61,20 @@ const DEFAULT_PARSER_OPTIONS = {
66
61
  */
67
62
  const SCHEMA_REF_PREFIX = "#/components/schemas/";
68
63
  /**
64
+ * HTTP methods that count as operations on an OpenAPI path item. Other keys
65
+ * (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
66
+ */
67
+ const SUPPORTED_METHODS = new Set([
68
+ "get",
69
+ "put",
70
+ "post",
71
+ "delete",
72
+ "options",
73
+ "head",
74
+ "patch",
75
+ "trace"
76
+ ]);
77
+ /**
69
78
  * OpenAPI version string written into the stub document created during multi-spec merges.
70
79
  */
71
80
  const MERGE_OPENAPI_VERSION = "3.0.0";
@@ -178,77 +187,103 @@ const typeOptionMap = new Map([
178
187
  function toCamelOrPascal(text, pascal) {
179
188
  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) => {
180
189
  if (word.length > 1 && word === word.toUpperCase()) return word;
181
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
182
- return word.charAt(0).toUpperCase() + word.slice(1);
190
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
183
191
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
184
192
  }
185
193
  /**
186
- * Splits `text` on `.` and applies `transformPart` to each segment.
187
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
188
- * Segments are joined with `/` to form a file path.
194
+ * Converts `text` to camelCase.
189
195
  *
190
- * Only splits on dots followed by a letter so that version numbers
191
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
196
+ * @example Word boundaries
197
+ * `camelCase('hello-world') // 'helloWorld'`
192
198
  *
193
- * Empty segments are filtered before joining. They arise when the text starts with
194
- * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
195
- * and `'..'` transforms to an empty string). Without this filter the join would produce
196
- * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
197
- * generated files to escape the configured output directory.
199
+ * @example With a prefix
200
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
198
201
  */
199
- function applyToFileParts(text, transformPart) {
200
- const parts = text.split(/\.(?=[a-zA-Z])/);
201
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
202
- }
203
- /**
204
- * Converts `text` to camelCase.
205
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
206
- *
207
- * @example
208
- * camelCase('hello-world') // 'helloWorld'
209
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
210
- */
211
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
212
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
213
- prefix,
214
- suffix
215
- } : {}));
202
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
216
203
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
217
204
  }
218
205
  /**
219
206
  * Converts `text` to PascalCase.
220
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
221
207
  *
222
- * @example
223
- * pascalCase('hello-world') // 'HelloWorld'
224
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
225
- */
226
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
227
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
228
- prefix,
229
- suffix
230
- }) : camelCase(part));
208
+ * @example Word boundaries
209
+ * `pascalCase('hello-world') // 'HelloWorld'`
210
+ *
211
+ * @example With a suffix
212
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
213
+ */
214
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
231
215
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
232
216
  }
233
217
  //#endregion
234
218
  //#region ../../internals/utils/src/runtime.ts
235
219
  /**
236
- * Returns `true` when the current process is running under Bun.
237
- *
238
- * Detection keys off the global `Bun` object rather than `process.versions`,
239
- * because Bun polyfills `process.versions.node` for Node compatibility and would
240
- * otherwise look like Node.
220
+ * Detects the JavaScript runtime executing the current process and exposes its name and version.
241
221
  *
242
- * @example
243
- * ```ts
244
- * if (isBun()) {
245
- * await Bun.write(path, data)
246
- * }
247
- * ```
222
+ * Prefer the shared {@link runtime} instance over constructing your own.
248
223
  */
249
- function isBun() {
250
- return typeof Bun !== "undefined";
251
- }
224
+ var Runtime = class {
225
+ /**
226
+ * `true` when the current process is running under Bun.
227
+ *
228
+ * Detection keys off the global `Bun` object rather than `process.versions`,
229
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
230
+ * otherwise look like Node.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * if (runtime.isBun) {
235
+ * await Bun.write(path, data)
236
+ * }
237
+ * ```
238
+ */
239
+ get isBun() {
240
+ return typeof Bun !== "undefined";
241
+ }
242
+ /**
243
+ * `true` when the current process is running under Deno.
244
+ */
245
+ get isDeno() {
246
+ return typeof globalThis.Deno !== "undefined";
247
+ }
248
+ /**
249
+ * `true` when the current process is running under Node.
250
+ *
251
+ * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
252
+ */
253
+ get isNode() {
254
+ return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
255
+ }
256
+ /**
257
+ * Name of the runtime executing the current process.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
262
+ * ```
263
+ */
264
+ get name() {
265
+ if (this.isBun) return "bun";
266
+ if (this.isDeno) return "deno";
267
+ return "node";
268
+ }
269
+ /**
270
+ * Version of the active runtime, or an empty string when it cannot be read.
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
275
+ * ```
276
+ */
277
+ get version() {
278
+ if (this.isBun) return process.versions.bun ?? "";
279
+ if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
280
+ return process.versions?.node ?? "";
281
+ }
282
+ };
283
+ /**
284
+ * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
285
+ */
286
+ const runtime = new Runtime();
252
287
  //#endregion
253
288
  //#region ../../internals/utils/src/fs.ts
254
289
  /**
@@ -263,7 +298,7 @@ function isBun() {
263
298
  * ```
264
299
  */
265
300
  async function exists(path) {
266
- if (isBun()) return Bun.file(path).exists();
301
+ if (runtime.isBun) return Bun.file(path).exists();
267
302
  return (0, node_fs_promises.access)(path).then(() => true, () => false);
268
303
  }
269
304
  /**
@@ -276,7 +311,7 @@ async function exists(path) {
276
311
  * ```
277
312
  */
278
313
  async function read(path) {
279
- if (isBun()) return Bun.file(path).text();
314
+ if (runtime.isBun) return Bun.file(path).text();
280
315
  return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
281
316
  }
282
317
  //#endregion
@@ -433,99 +468,80 @@ function isIdentifier(name) {
433
468
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
434
469
  }
435
470
  //#endregion
436
- //#region ../../internals/utils/src/urlPath.ts
471
+ //#region ../../internals/utils/src/url.ts
472
+ function transformParam(raw, casing) {
473
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
474
+ return casing === "camelcase" ? camelCase(param) : param;
475
+ }
476
+ function toParamsObject(path, { replacer, casing } = {}) {
477
+ const params = {};
478
+ for (const match of path.matchAll(/\{([^}]+)\}/g)) {
479
+ const param = transformParam(match[1], casing);
480
+ const key = replacer ? replacer(param) : param;
481
+ params[key] = key;
482
+ }
483
+ return Object.keys(params).length > 0 ? params : null;
484
+ }
437
485
  /**
438
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
439
- *
440
- * @example
441
- * const p = new URLPath('/pet/{petId}')
442
- * p.URL // '/pet/:petId'
443
- * p.template // '`/pet/${petId}`'
486
+ * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
444
487
  */
445
- var URLPath = class {
488
+ var Url = class Url {
446
489
  /**
447
- * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
448
- */
449
- path;
450
- #options;
451
- constructor(path, options = {}) {
452
- this.path = path;
453
- this.#options = options;
454
- }
455
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
490
+ * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
456
491
  *
457
492
  * @example
458
- * ```ts
459
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
460
- * ```
493
+ * Url.canParse('https://petstore.swagger.io/v2') // true
494
+ * Url.canParse('/pet/{petId}') // false
461
495
  */
462
- get URL() {
463
- return this.toURLPath();
496
+ static canParse(url, base) {
497
+ return URL.canParse(url, base);
464
498
  }
465
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
499
+ /**
500
+ * Converts an OpenAPI/Swagger path to Express-style colon syntax.
466
501
  *
467
502
  * @example
468
- * ```ts
469
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
470
- * new URLPath('/pet/{petId}').isURL // false
471
- * ```
503
+ * Url.toPath('/pet/{petId}') // '/pet/:petId'
472
504
  */
473
- get isURL() {
474
- try {
475
- return !!new URL(this.path).href;
476
- } catch {
477
- return false;
478
- }
505
+ static toPath(path) {
506
+ return path.replace(/\{([^}]+)\}/g, ":$1");
479
507
  }
480
508
  /**
481
- * Converts the OpenAPI path to a TypeScript template literal string.
509
+ * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
510
+ * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
511
+ * and `casing` controls parameter identifier casing.
482
512
  *
483
513
  * @example
484
- * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
485
- * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
486
- */
487
- get template() {
488
- return this.toTemplateString();
489
- }
490
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
514
+ * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
491
515
  *
492
516
  * @example
493
- * ```ts
494
- * new URLPath('/pet/{petId}').object
495
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
496
- * ```
517
+ * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
497
518
  */
498
- get object() {
499
- return this.toObject();
519
+ static toTemplateString(path, { prefix, replacer, casing } = {}) {
520
+ const result = path.split(/\{([^}]+)\}/).map((part, i) => {
521
+ if (i % 2 === 0) return part;
522
+ const param = transformParam(part, casing);
523
+ return `\${${replacer ? replacer(param) : param}}`;
524
+ }).join("");
525
+ return `\`${prefix ?? ""}${result}\``;
500
526
  }
501
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
527
+ /**
528
+ * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
529
+ * expression when `stringify` is set.
502
530
  *
503
531
  * @example
504
- * ```ts
505
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
506
- * new URLPath('/pet').params // undefined
507
- * ```
508
- */
509
- get params() {
510
- return this.getParams();
511
- }
512
- #transformParam(raw) {
513
- const param = isValidVarName(raw) ? raw : camelCase(raw);
514
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
515
- }
516
- /**
517
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
532
+ * Url.toObject('/pet/{petId}')
533
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
518
534
  */
519
- #eachParam(fn) {
520
- for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
521
- const raw = match[1];
522
- fn(raw, this.#transformParam(raw));
523
- }
524
- }
525
- toObject({ type = "path", replacer, stringify } = {}) {
535
+ static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
526
536
  const object = {
527
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
528
- params: this.getParams()
537
+ url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
538
+ replacer,
539
+ casing
540
+ }),
541
+ params: toParamsObject(path, {
542
+ replacer,
543
+ casing
544
+ })
529
545
  };
530
546
  if (stringify) {
531
547
  if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
@@ -534,50 +550,6 @@ var URLPath = class {
534
550
  }
535
551
  return object;
536
552
  }
537
- /**
538
- * Converts the OpenAPI path to a TypeScript template literal string.
539
- * An optional `replacer` can transform each extracted parameter name before interpolation.
540
- *
541
- * @example
542
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
543
- */
544
- toTemplateString({ prefix, replacer } = {}) {
545
- const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
546
- if (i % 2 === 0) return part;
547
- const param = this.#transformParam(part);
548
- return `\${${replacer ? replacer(param) : param}}`;
549
- }).join("");
550
- return `\`${prefix ?? ""}${result}\``;
551
- }
552
- /**
553
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
554
- * An optional `replacer` transforms each parameter name in both key and value positions.
555
- * Returns `undefined` when no path parameters are found.
556
- *
557
- * @example
558
- * ```ts
559
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
560
- * // { petId: 'petId', tagId: 'tagId' }
561
- * ```
562
- */
563
- getParams(replacer) {
564
- const params = {};
565
- this.#eachParam((_raw, param) => {
566
- const key = replacer ? replacer(param) : param;
567
- params[key] = key;
568
- });
569
- return Object.keys(params).length > 0 ? params : void 0;
570
- }
571
- /** Converts the OpenAPI path to Express-style colon syntax.
572
- *
573
- * @example
574
- * ```ts
575
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
576
- * ```
577
- */
578
- toURLPath() {
579
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
580
- }
581
553
  };
582
554
  //#endregion
583
555
  //#region src/bundler.ts
@@ -594,7 +566,7 @@ async function readSource(sourcePath) {
594
566
  async function resolveSource(sourcePath) {
595
567
  const data = await readSource(sourcePath);
596
568
  if (sourcePath.toLowerCase().endsWith(".md")) return data;
597
- return js_yaml.default.load(data);
569
+ return (0, yaml.parse)(data);
598
570
  }
599
571
  /**
600
572
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
@@ -699,15 +671,9 @@ function isDiscriminator(obj) {
699
671
  * const document = await parse(rawDocumentObject, { canBundle: false })
700
672
  * ```
701
673
  */
702
- async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true } = {}) {
703
- if (typeof pathOrApi === "string" && canBundle) return parseDocument(await bundleDocument(pathOrApi), {
704
- canBundle: false,
705
- enablePaths
706
- });
707
- const document = await new oas_normalize.default(pathOrApi, {
708
- enablePaths,
709
- colorizeErrors: true
710
- }).load();
674
+ async function parseDocument(pathOrApi, { canBundle = true } = {}) {
675
+ if (typeof pathOrApi === "string" && canBundle) return parseDocument(await bundleDocument(pathOrApi), { canBundle: false });
676
+ const document = typeof pathOrApi === "string" ? (0, yaml.parse)(pathOrApi) : pathOrApi;
711
677
  if (isOpenApiV2Document(document)) {
712
678
  const { default: swagger2openapi } = await import("swagger2openapi");
713
679
  const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
@@ -718,7 +684,7 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
718
684
  /**
719
685
  * Deep-merges multiple OpenAPI documents into a single `Document`.
720
686
  *
721
- * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
687
+ * Each document is parsed independently, then deep-merged into one in array order.
722
688
  * Throws when the input array is empty.
723
689
  *
724
690
  * @example
@@ -727,10 +693,7 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
727
693
  * ```
728
694
  */
729
695
  async function mergeDocuments(pathOrApi) {
730
- const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, {
731
- enablePaths: false,
732
- canBundle: false
733
- })));
696
+ const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })));
734
697
  if (documents.length === 0) throw new _kubb_core.Diagnostics.Error({
735
698
  code: _kubb_core.Diagnostics.code.inputRequired,
736
699
  severity: "error",
@@ -769,7 +732,7 @@ async function parseFromConfig(source) {
769
732
  return parseDocument(source.data, { canBundle: false });
770
733
  }
771
734
  if (source.type === "paths") return mergeDocuments(source.paths);
772
- if (new URLPath(source.path).isURL) return parseDocument(source.path);
735
+ if (Url.canParse(source.path)) return parseDocument(source.path);
773
736
  const resolved = node_path.default.resolve(node_path.default.dirname(source.path), source.path);
774
737
  await assertInputExists(resolved);
775
738
  return parseDocument(resolved);
@@ -780,7 +743,7 @@ async function parseFromConfig(source) {
780
743
  * its parse error instead.
781
744
  */
782
745
  async function assertInputExists(input) {
783
- if (new URLPath(input).isURL) return;
746
+ if (Url.canParse(input)) return;
784
747
  if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
785
748
  code: _kubb_core.Diagnostics.code.inputNotFound,
786
749
  severity: "error",
@@ -790,7 +753,7 @@ async function assertInputExists(input) {
790
753
  });
791
754
  }
792
755
  /**
793
- * Validates an OpenAPI document using `oas-normalize` with colorized error output.
756
+ * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
794
757
  *
795
758
  * @example
796
759
  * ```ts
@@ -799,10 +762,8 @@ async function assertInputExists(input) {
799
762
  */
800
763
  async function validateDocument(document, { throwOnError = false } = {}) {
801
764
  try {
802
- await new oas_normalize.default(document, {
803
- enablePaths: true,
804
- colorizeErrors: true
805
- }).validate({ parser: { validate: { errors: { colorize: true } } } });
765
+ const result = await (0, _readme_openapi_parser.validate)(structuredClone(document), { validate: { errors: { colorize: true } } });
766
+ if (!result.valid) throw new Error((0, _readme_openapi_parser.compileErrors)(result));
806
767
  } catch (error) {
807
768
  if (throwOnError) throw error;
808
769
  }
@@ -902,6 +863,161 @@ const oasDialect = _kubb_core.ast.defineSchemaDialect({
902
863
  resolveRef
903
864
  });
904
865
  //#endregion
866
+ //#region src/mime.ts
867
+ /**
868
+ * MIME type fragments that mark a media type as JSON-like.
869
+ *
870
+ * Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
871
+ * JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
872
+ * `application/vnd.api+json`.
873
+ */
874
+ const jsonMimeFragments = [
875
+ "application/json",
876
+ "application/x-json",
877
+ "text/json",
878
+ "text/x-json",
879
+ "+json"
880
+ ];
881
+ /**
882
+ * Returns `true` when a media type string is JSON-like.
883
+ *
884
+ * @example
885
+ * ```ts
886
+ * isJsonMimeType('application/json') // true
887
+ * isJsonMimeType('application/vnd.api+json') // true
888
+ * isJsonMimeType('multipart/form-data') // false
889
+ * ```
890
+ */
891
+ function isJsonMimeType(mimeType) {
892
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
893
+ }
894
+ //#endregion
895
+ //#region src/operation.ts
896
+ /**
897
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
898
+ * with no leading or trailing dash.
899
+ */
900
+ function slugify(value) {
901
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
902
+ }
903
+ /**
904
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
905
+ */
906
+ function getOperationId({ path, method, schema }) {
907
+ const { operationId } = schema;
908
+ if (typeof operationId === "string" && operationId.length > 0) return operationId;
909
+ return `${method}_${slugify(path).toLowerCase()}`;
910
+ }
911
+ /**
912
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
913
+ */
914
+ function getResponseStatusCodes({ schema }) {
915
+ const responses = schema.responses;
916
+ if (!responses || isReference(responses)) return [];
917
+ return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
918
+ }
919
+ /**
920
+ * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
921
+ */
922
+ function getResponseByStatusCode({ document, operation, statusCode }) {
923
+ const responses = operation.schema.responses;
924
+ if (!responses || isReference(responses)) return false;
925
+ const response = responses[statusCode];
926
+ if (!response) return false;
927
+ if (isReference(response)) {
928
+ const resolved = resolveRef(document, response.$ref);
929
+ responses[statusCode] = resolved;
930
+ if (!resolved || isReference(resolved)) return false;
931
+ return resolved;
932
+ }
933
+ return response;
934
+ }
935
+ /**
936
+ * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
937
+ * `undefined` when the operation has no request body.
938
+ */
939
+ function getRequestBodyContent({ document, operation }) {
940
+ const { schema } = operation;
941
+ let requestBody = schema.requestBody;
942
+ if (!requestBody) return;
943
+ if (isReference(requestBody)) {
944
+ const resolved = resolveRef(document, requestBody.$ref);
945
+ schema.requestBody = resolved;
946
+ if (!resolved || isReference(resolved)) return;
947
+ requestBody = resolved;
948
+ }
949
+ return requestBody.content;
950
+ }
951
+ /**
952
+ * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
953
+ * Otherwise picks the first JSON-like media type, then the first declared one, as a
954
+ * `[mediaType, object]` tuple.
955
+ */
956
+ function getRequestContent({ document, operation, mediaType }) {
957
+ const content = getRequestBodyContent({
958
+ document,
959
+ operation
960
+ });
961
+ if (!content) return false;
962
+ if (mediaType) return mediaType in content ? content[mediaType] : false;
963
+ const mediaTypes = Object.keys(content);
964
+ const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
965
+ return available ? [available, content[available]] : false;
966
+ }
967
+ /**
968
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
969
+ * matching the previous behavior), then the first declared one, defaulting to `'application/json'`.
970
+ */
971
+ function getRequestContentType({ document, operation }) {
972
+ const content = getRequestBodyContent({
973
+ document,
974
+ operation
975
+ });
976
+ const mediaTypes = content ? Object.keys(content) : [];
977
+ let result = mediaTypes[0] ?? "application/json";
978
+ for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
979
+ return result;
980
+ }
981
+ /**
982
+ * Builds an `Operation` for every supported HTTP method on every path, in document order.
983
+ * `x-` path keys and unresolvable path-item `$ref`s are skipped.
984
+ *
985
+ * @example
986
+ * ```ts
987
+ * for (const operation of getOperations(document)) {
988
+ * parseOperation(options, operation)
989
+ * }
990
+ * ```
991
+ */
992
+ function getOperations(document) {
993
+ const operations = [];
994
+ const paths = document.paths;
995
+ if (!paths) return operations;
996
+ for (const path of Object.keys(paths)) {
997
+ if (path.startsWith("x-")) continue;
998
+ let pathItem = paths[path];
999
+ if (!pathItem) continue;
1000
+ if (isReference(pathItem)) {
1001
+ const resolved = resolveRef(document, pathItem.$ref);
1002
+ paths[path] = resolved;
1003
+ if (!resolved || isReference(resolved)) continue;
1004
+ pathItem = resolved;
1005
+ }
1006
+ const item = pathItem;
1007
+ for (const method of Object.keys(item)) {
1008
+ if (!SUPPORTED_METHODS.has(method)) continue;
1009
+ const schema = item[method];
1010
+ if (!schema || typeof schema !== "object") continue;
1011
+ operations.push({
1012
+ path,
1013
+ method,
1014
+ schema
1015
+ });
1016
+ }
1017
+ }
1018
+ return operations;
1019
+ }
1020
+ //#endregion
905
1021
  //#region src/resolvers.ts
906
1022
  /**
907
1023
  * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
@@ -994,7 +1110,7 @@ function getResponseBody(responseBody, contentType) {
994
1110
  }
995
1111
  let availableContentType;
996
1112
  const contentTypes = Object.keys(body.content);
997
- for (const mt of contentTypes) if (oas_utils.matchesMimeType.json(mt)) {
1113
+ for (const mt of contentTypes) if (isJsonMimeType(mt)) {
998
1114
  availableContentType = mt;
999
1115
  break;
1000
1116
  }
@@ -1025,7 +1141,11 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
1025
1141
  if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
1026
1142
  }
1027
1143
  }
1028
- const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
1144
+ const responseBody = getResponseBody(getResponseByStatusCode({
1145
+ document,
1146
+ operation,
1147
+ statusCode
1148
+ }), options.contentType);
1029
1149
  if (responseBody === false) return {};
1030
1150
  const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
1031
1151
  if (!schema) return {};
@@ -1041,7 +1161,11 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
1041
1161
  */
1042
1162
  function getRequestSchema(document, operation, options = {}) {
1043
1163
  if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
1044
- const requestBody = operation.getRequestBody(options.contentType);
1164
+ const requestBody = getRequestContent({
1165
+ document,
1166
+ operation,
1167
+ mediaType: options.contentType
1168
+ });
1045
1169
  if (requestBody === false) return null;
1046
1170
  const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
1047
1171
  if (!schema) return null;
@@ -1076,7 +1200,7 @@ function hasStructuralKeywords(fragment) {
1076
1200
  function flattenSchema(schema) {
1077
1201
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
1078
1202
  const allOfFragments = schema.allOf;
1079
- if (allOfFragments.some((item) => (0, oas_types.isRef)(item))) return schema;
1203
+ if (allOfFragments.some((item) => isReference(item))) return schema;
1080
1204
  if (allOfFragments.some(hasStructuralKeywords)) return schema;
1081
1205
  const merged = { ...schema };
1082
1206
  delete merged.allOf;
@@ -1314,7 +1438,11 @@ function getResponseBodyContentTypes(document, operation, statusCode) {
1314
1438
  if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
1315
1439
  }
1316
1440
  }
1317
- const responseObj = operation.getResponseByStatusCode(statusCode);
1441
+ const responseObj = getResponseByStatusCode({
1442
+ document,
1443
+ operation,
1444
+ statusCode
1445
+ });
1318
1446
  if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1319
1447
  const body = responseObj;
1320
1448
  return body.content ? Object.keys(body.content) : [];
@@ -1898,7 +2026,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1898
2026
  });
1899
2027
  }
1900
2028
  /**
1901
- * Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
2029
+ * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
1902
2030
  * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1903
2031
  * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1904
2032
  * match/convert/fall-through contract.
@@ -1996,10 +2124,10 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1996
2124
  }
1997
2125
  ];
1998
2126
  /**
1999
- * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
2127
+ * Converts an OAS `SchemaObject` into a `SchemaNode`.
2000
2128
  *
2001
- * Builds the per-schema context, then runs it through the ordered {@link schemaRules} table
2002
- * via {@link ast.dispatch}. When no rule produces a node, falls back to the configured
2129
+ * Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
2130
+ * the first converter that produces a node. When none match, falls back to the configured
2003
2131
  * `emptySchemaType`.
2004
2132
  */
2005
2133
  function parseSchema({ schema, name }, rawOptions) {
@@ -2022,8 +2150,11 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2022
2150
  rawOptions,
2023
2151
  options
2024
2152
  };
2025
- const node = _kubb_core.ast.dispatch(schemaRules, schemaCtx);
2026
- if (node) return node;
2153
+ for (const rule of schemaRules) {
2154
+ if (!rule.match(schemaCtx)) continue;
2155
+ const node = rule.convert(schemaCtx);
2156
+ if (node) return node;
2157
+ }
2027
2158
  const emptyType = typeOptionMap.get(options.emptySchemaType);
2028
2159
  return _kubb_core.ast.createSchema({
2029
2160
  type: emptyType,
@@ -2094,7 +2225,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2094
2225
  * Converts an OAS `Operation` into an `OperationNode`.
2095
2226
  */
2096
2227
  function parseOperation(options, operation) {
2097
- const operationId = operation.getOperationId();
2228
+ const operationId = getOperationId(operation);
2098
2229
  const operationName = operationId ? pascalCase(operationId) : void 0;
2099
2230
  const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
2100
2231
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
@@ -2117,8 +2248,12 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2117
2248
  required: requestBodyMeta.required || void 0,
2118
2249
  content: content.length > 0 ? content : void 0
2119
2250
  } : void 0;
2120
- const responses = operation.getResponseStatusCodes().map((statusCode) => {
2121
- const responseObj = operation.getResponseByStatusCode(statusCode);
2251
+ const responses = getResponseStatusCodes(operation).map((statusCode) => {
2252
+ const responseObj = getResponseByStatusCode({
2253
+ document,
2254
+ operation,
2255
+ statusCode
2256
+ });
2122
2257
  const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2123
2258
  const { description } = getResponseMeta(responseObj);
2124
2259
  const parseEntrySchema = (contentType) => {
@@ -2136,7 +2271,10 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2136
2271
  ...parseEntrySchema(contentType)
2137
2272
  }));
2138
2273
  if (content.length === 0) content.push({
2139
- contentType: operation.contentType || "application/json",
2274
+ contentType: getRequestContentType({
2275
+ document,
2276
+ operation
2277
+ }) || "application/json",
2140
2278
  ...parseEntrySchema(ctx.contentType)
2141
2279
  });
2142
2280
  return _kubb_core.ast.createResponse({
@@ -2145,16 +2283,23 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2145
2283
  content
2146
2284
  });
2147
2285
  });
2148
- const urlPath = new URLPath(operation.path);
2286
+ const pathItem = document.paths?.[operation.path];
2287
+ const pathItemDoc = pathItem && !dialect.isReference(pathItem) ? pathItem : void 0;
2288
+ const pickDoc = (key) => {
2289
+ const own = operation.schema[key];
2290
+ if (typeof own === "string") return own;
2291
+ const fallback = pathItemDoc?.[key];
2292
+ return typeof fallback === "string" ? fallback : void 0;
2293
+ };
2149
2294
  return _kubb_core.ast.createOperation({
2150
2295
  operationId,
2151
2296
  protocol: "http",
2152
2297
  method: operation.method.toUpperCase(),
2153
- path: urlPath.path,
2154
- tags: operation.getTags().map((tag) => tag.name),
2155
- summary: operation.getSummary() || void 0,
2156
- description: operation.getDescription() || void 0,
2157
- deprecated: operation.isDeprecated() || void 0,
2298
+ path: operation.path,
2299
+ tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
2300
+ summary: pickDoc("summary") || void 0,
2301
+ description: pickDoc("description") || void 0,
2302
+ deprecated: operation.schema.deprecated || void 0,
2158
2303
  parameters,
2159
2304
  requestBody,
2160
2305
  responses
@@ -2372,7 +2517,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2372
2517
  * })
2373
2518
  * ```
2374
2519
  */
2375
- function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe }) {
2520
+ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions, discriminator, dedupe }) {
2376
2521
  const allNodes = [];
2377
2522
  const refAliasMap = /* @__PURE__ */ new Map();
2378
2523
  const enumNames = [];
@@ -2396,8 +2541,7 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2396
2541
  let dedupePlan = null;
2397
2542
  if (dedupe) {
2398
2543
  const operationNodes = [];
2399
- for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2400
- if (!operation) continue;
2544
+ for (const operation of getOperations(document)) {
2401
2545
  const operationNode = parseOperation(parserOptions, operation);
2402
2546
  if (operationNode) operationNodes.push(operationNode);
2403
2547
  }
@@ -2419,7 +2563,7 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2419
2563
  };
2420
2564
  }
2421
2565
  /**
2422
- * Creates a lazy `InputStreamNode` from already-resolved adapter state.
2566
+ * Creates a lazy `InputNode<true>` from already-resolved adapter state.
2423
2567
  *
2424
2568
  * The schema and operation iterables each start a fresh parse pass on every
2425
2569
  * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
@@ -2430,16 +2574,16 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2430
2574
  *
2431
2575
  * @example
2432
2576
  * ```ts
2433
- * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, meta })
2577
+ * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, meta })
2434
2578
  * for await (const schema of streamNode.schemas) {
2435
2579
  * // each call to for-await restarts from the first schema
2436
2580
  * }
2437
2581
  * ```
2438
2582
  */
2439
- function createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2583
+ function createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2440
2584
  const rewriteTopLevelSchema = (node) => {
2441
2585
  if (!dedupePlan) return node;
2442
- const canonical = dedupePlan.canonicalBySignature.get(_kubb_core.ast.schemaSignature(node));
2586
+ const canonical = dedupePlan.canonicalBySignature.get(_kubb_core.ast.signatureOf(node));
2443
2587
  if (canonical && canonical.name !== node.name) return _kubb_core.ast.createSchema({
2444
2588
  type: "ref",
2445
2589
  name: node.name ?? null,
@@ -2475,8 +2619,7 @@ function createInputStream({ schemas, parseSchema, parseOperation, baseOas, pars
2475
2619
  } };
2476
2620
  const operationsIterable = { [Symbol.asyncIterator]() {
2477
2621
  return (async function* () {
2478
- for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2479
- if (!operation) continue;
2622
+ for (const operation of getOperations(document)) {
2480
2623
  const node = parseOperation(parserOptions, operation);
2481
2624
  if (node) yield dedupePlan ? _kubb_core.ast.applyDedupe(node, dedupePlan) : node;
2482
2625
  }
@@ -2531,7 +2674,6 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2531
2674
  let parsedDocument = null;
2532
2675
  const documentCache = /* @__PURE__ */ new WeakMap();
2533
2676
  const schemasCache = /* @__PURE__ */ new WeakMap();
2534
- const baseOasCache = /* @__PURE__ */ new WeakMap();
2535
2677
  const schemaParserCache = /* @__PURE__ */ new WeakMap();
2536
2678
  const preScanCache = /* @__PURE__ */ new WeakMap();
2537
2679
  function ensureDocument(source) {
@@ -2557,13 +2699,6 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2557
2699
  schemasCache.set(document, promise);
2558
2700
  return promise;
2559
2701
  }
2560
- function ensureBaseOas(document) {
2561
- const cached = baseOasCache.get(document);
2562
- if (cached) return cached;
2563
- const baseOas = new oas.default(document);
2564
- baseOasCache.set(document, baseOas);
2565
- return baseOas;
2566
- }
2567
2702
  function ensureSchemaParser(document) {
2568
2703
  const cached = schemaParserCache.get(document);
2569
2704
  if (cached) return cached;
@@ -2574,14 +2709,14 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2574
2709
  schemaParserCache.set(document, parser);
2575
2710
  return parser;
2576
2711
  }
2577
- function ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas) {
2712
+ function ensurePreScan(document, schemas, parseSchema, parseOperation) {
2578
2713
  const cached = preScanCache.get(document);
2579
2714
  if (cached) return cached;
2580
2715
  const result = preScan({
2581
2716
  schemas,
2582
2717
  parseSchema,
2583
2718
  parseOperation,
2584
- baseOas,
2719
+ document,
2585
2720
  parserOptions,
2586
2721
  discriminator,
2587
2722
  dedupe
@@ -2593,13 +2728,12 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2593
2728
  const document = await ensureDocument(source);
2594
2729
  const schemas = await ensureSchemas(document);
2595
2730
  const { parseSchema, parseOperation } = ensureSchemaParser(document);
2596
- const baseOas = ensureBaseOas(document);
2597
- const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas);
2731
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation);
2598
2732
  return createInputStream({
2599
2733
  schemas,
2600
2734
  parseSchema,
2601
2735
  parseOperation,
2602
- baseOas,
2736
+ document,
2603
2737
  parserOptions,
2604
2738
  refAliasMap,
2605
2739
  discriminatorChildMap,
@@ -2644,18 +2778,17 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2644
2778
  await validateDocument(await parseDocument(input), options);
2645
2779
  },
2646
2780
  getImports(node, resolve) {
2647
- return _kubb_core.ast.collectImports({
2648
- node,
2649
- nameMapping,
2650
- resolve: (schemaName) => {
2651
- const result = resolve(schemaName);
2652
- if (!result) return null;
2653
- return _kubb_core.ast.createImport({
2654
- name: [result.name],
2655
- path: result.path
2656
- });
2657
- }
2658
- });
2781
+ return (0, _kubb_ast.collect)(node, { schema(schemaNode) {
2782
+ const schemaRef = (0, _kubb_ast.narrowSchema)(schemaNode, "ref");
2783
+ if (!schemaRef?.ref) return null;
2784
+ const rawName = (0, _kubb_ast_utils.extractRefName)(schemaRef.ref);
2785
+ const result = resolve(nameMapping.get(rawName) ?? rawName);
2786
+ if (!result) return null;
2787
+ return _kubb_core.ast.createImport({
2788
+ name: [result.name],
2789
+ path: result.path
2790
+ });
2791
+ } });
2659
2792
  },
2660
2793
  async parse(source) {
2661
2794
  const streamNode = await createStream(source);