@kubb/adapter-oas 5.0.0-beta.53 → 5.0.0-beta.55

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.js CHANGED
@@ -1,14 +1,12 @@
1
1
  import "./chunk-C0LytTxp.js";
2
2
  import { Diagnostics, ast, createAdapter } from "@kubb/core";
3
- import BaseOas from "oas";
4
3
  import path from "node:path";
5
4
  import { access, readFile } from "node:fs/promises";
6
- import OASNormalize from "oas-normalize";
5
+ import { compileErrors, validate } from "@readme/openapi-parser";
6
+ import { parse } from "yaml";
7
7
  import { bundle } from "api-ref-bundler";
8
- import yaml from "js-yaml";
9
8
  import { childName, enumPropName, extractRefName, findDiscriminator } from "@kubb/ast/utils";
10
- import { isRef } from "oas/types";
11
- import { matchesMimeType } from "oas/utils";
9
+ import { collect, narrowSchema } from "@kubb/ast";
12
10
  //#region src/constants.ts
13
11
  /**
14
12
  * Default parser options applied when no explicit options are provided.
@@ -40,6 +38,20 @@ const DEFAULT_PARSER_OPTIONS = {
40
38
  */
41
39
  const SCHEMA_REF_PREFIX = "#/components/schemas/";
42
40
  /**
41
+ * HTTP methods that count as operations on an OpenAPI path item. Other keys
42
+ * (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
43
+ */
44
+ const SUPPORTED_METHODS = new Set([
45
+ "get",
46
+ "put",
47
+ "post",
48
+ "delete",
49
+ "options",
50
+ "head",
51
+ "patch",
52
+ "trace"
53
+ ]);
54
+ /**
43
55
  * OpenAPI version string written into the stub document created during multi-spec merges.
44
56
  */
45
57
  const MERGE_OPENAPI_VERSION = "3.0.0";
@@ -152,77 +164,103 @@ const typeOptionMap = new Map([
152
164
  function toCamelOrPascal(text, pascal) {
153
165
  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) => {
154
166
  if (word.length > 1 && word === word.toUpperCase()) return word;
155
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
156
- return word.charAt(0).toUpperCase() + word.slice(1);
167
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
157
168
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
158
169
  }
159
170
  /**
160
- * Splits `text` on `.` and applies `transformPart` to each segment.
161
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
162
- * Segments are joined with `/` to form a file path.
171
+ * Converts `text` to camelCase.
163
172
  *
164
- * Only splits on dots followed by a letter so that version numbers
165
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
173
+ * @example Word boundaries
174
+ * `camelCase('hello-world') // 'helloWorld'`
166
175
  *
167
- * Empty segments are filtered before joining. They arise when the text starts with
168
- * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
169
- * and `'..'` transforms to an empty string). Without this filter the join would produce
170
- * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
171
- * generated files to escape the configured output directory.
176
+ * @example With a prefix
177
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
172
178
  */
173
- function applyToFileParts(text, transformPart) {
174
- const parts = text.split(/\.(?=[a-zA-Z])/);
175
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
176
- }
177
- /**
178
- * Converts `text` to camelCase.
179
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
180
- *
181
- * @example
182
- * camelCase('hello-world') // 'helloWorld'
183
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
184
- */
185
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
186
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
187
- prefix,
188
- suffix
189
- } : {}));
179
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
190
180
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
191
181
  }
192
182
  /**
193
183
  * Converts `text` to PascalCase.
194
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
195
184
  *
196
- * @example
197
- * pascalCase('hello-world') // 'HelloWorld'
198
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
199
- */
200
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
201
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
202
- prefix,
203
- suffix
204
- }) : camelCase(part));
185
+ * @example Word boundaries
186
+ * `pascalCase('hello-world') // 'HelloWorld'`
187
+ *
188
+ * @example With a suffix
189
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
190
+ */
191
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
205
192
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
206
193
  }
207
194
  //#endregion
208
195
  //#region ../../internals/utils/src/runtime.ts
209
196
  /**
210
- * Returns `true` when the current process is running under Bun.
211
- *
212
- * Detection keys off the global `Bun` object rather than `process.versions`,
213
- * because Bun polyfills `process.versions.node` for Node compatibility and would
214
- * otherwise look like Node.
197
+ * Detects the JavaScript runtime executing the current process and exposes its name and version.
215
198
  *
216
- * @example
217
- * ```ts
218
- * if (isBun()) {
219
- * await Bun.write(path, data)
220
- * }
221
- * ```
199
+ * Prefer the shared {@link runtime} instance over constructing your own.
222
200
  */
223
- function isBun() {
224
- return typeof Bun !== "undefined";
225
- }
201
+ var Runtime = class {
202
+ /**
203
+ * `true` when the current process is running under Bun.
204
+ *
205
+ * Detection keys off the global `Bun` object rather than `process.versions`,
206
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
207
+ * otherwise look like Node.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * if (runtime.isBun) {
212
+ * await Bun.write(path, data)
213
+ * }
214
+ * ```
215
+ */
216
+ get isBun() {
217
+ return typeof Bun !== "undefined";
218
+ }
219
+ /**
220
+ * `true` when the current process is running under Deno.
221
+ */
222
+ get isDeno() {
223
+ return typeof globalThis.Deno !== "undefined";
224
+ }
225
+ /**
226
+ * `true` when the current process is running under Node.
227
+ *
228
+ * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
229
+ */
230
+ get isNode() {
231
+ return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
232
+ }
233
+ /**
234
+ * Name of the runtime executing the current process.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
239
+ * ```
240
+ */
241
+ get name() {
242
+ if (this.isBun) return "bun";
243
+ if (this.isDeno) return "deno";
244
+ return "node";
245
+ }
246
+ /**
247
+ * Version of the active runtime, or an empty string when it cannot be read.
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
252
+ * ```
253
+ */
254
+ get version() {
255
+ if (this.isBun) return process.versions.bun ?? "";
256
+ if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
257
+ return process.versions?.node ?? "";
258
+ }
259
+ };
260
+ /**
261
+ * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
262
+ */
263
+ const runtime = new Runtime();
226
264
  //#endregion
227
265
  //#region ../../internals/utils/src/fs.ts
228
266
  /**
@@ -237,7 +275,7 @@ function isBun() {
237
275
  * ```
238
276
  */
239
277
  async function exists(path) {
240
- if (isBun()) return Bun.file(path).exists();
278
+ if (runtime.isBun) return Bun.file(path).exists();
241
279
  return access(path).then(() => true, () => false);
242
280
  }
243
281
  /**
@@ -250,7 +288,7 @@ async function exists(path) {
250
288
  * ```
251
289
  */
252
290
  async function read(path) {
253
- if (isBun()) return Bun.file(path).text();
291
+ if (runtime.isBun) return Bun.file(path).text();
254
292
  return readFile(path, { encoding: "utf8" });
255
293
  }
256
294
  //#endregion
@@ -407,99 +445,80 @@ function isIdentifier(name) {
407
445
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
408
446
  }
409
447
  //#endregion
410
- //#region ../../internals/utils/src/urlPath.ts
448
+ //#region ../../internals/utils/src/url.ts
449
+ function transformParam(raw, casing) {
450
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
451
+ return casing === "camelcase" ? camelCase(param) : param;
452
+ }
453
+ function toParamsObject(path, { replacer, casing } = {}) {
454
+ const params = {};
455
+ for (const match of path.matchAll(/\{([^}]+)\}/g)) {
456
+ const param = transformParam(match[1], casing);
457
+ const key = replacer ? replacer(param) : param;
458
+ params[key] = key;
459
+ }
460
+ return Object.keys(params).length > 0 ? params : null;
461
+ }
411
462
  /**
412
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
413
- *
414
- * @example
415
- * const p = new URLPath('/pet/{petId}')
416
- * p.URL // '/pet/:petId'
417
- * p.template // '`/pet/${petId}`'
463
+ * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
418
464
  */
419
- var URLPath = class {
465
+ var Url = class Url {
420
466
  /**
421
- * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
422
- */
423
- path;
424
- #options;
425
- constructor(path, options = {}) {
426
- this.path = path;
427
- this.#options = options;
428
- }
429
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
467
+ * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
430
468
  *
431
469
  * @example
432
- * ```ts
433
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
434
- * ```
470
+ * Url.canParse('https://petstore.swagger.io/v2') // true
471
+ * Url.canParse('/pet/{petId}') // false
435
472
  */
436
- get URL() {
437
- return this.toURLPath();
473
+ static canParse(url, base) {
474
+ return URL.canParse(url, base);
438
475
  }
439
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
476
+ /**
477
+ * Converts an OpenAPI/Swagger path to Express-style colon syntax.
440
478
  *
441
479
  * @example
442
- * ```ts
443
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
444
- * new URLPath('/pet/{petId}').isURL // false
445
- * ```
480
+ * Url.toPath('/pet/{petId}') // '/pet/:petId'
446
481
  */
447
- get isURL() {
448
- try {
449
- return !!new URL(this.path).href;
450
- } catch {
451
- return false;
452
- }
482
+ static toPath(path) {
483
+ return path.replace(/\{([^}]+)\}/g, ":$1");
453
484
  }
454
485
  /**
455
- * Converts the OpenAPI path to a TypeScript template literal string.
486
+ * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
487
+ * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
488
+ * and `casing` controls parameter identifier casing.
456
489
  *
457
490
  * @example
458
- * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
459
- * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
460
- */
461
- get template() {
462
- return this.toTemplateString();
463
- }
464
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
491
+ * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
465
492
  *
466
493
  * @example
467
- * ```ts
468
- * new URLPath('/pet/{petId}').object
469
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
470
- * ```
494
+ * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
471
495
  */
472
- get object() {
473
- return this.toObject();
496
+ static toTemplateString(path, { prefix, replacer, casing } = {}) {
497
+ const result = path.split(/\{([^}]+)\}/).map((part, i) => {
498
+ if (i % 2 === 0) return part;
499
+ const param = transformParam(part, casing);
500
+ return `\${${replacer ? replacer(param) : param}}`;
501
+ }).join("");
502
+ return `\`${prefix ?? ""}${result}\``;
474
503
  }
475
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
504
+ /**
505
+ * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
506
+ * expression when `stringify` is set.
476
507
  *
477
508
  * @example
478
- * ```ts
479
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
480
- * new URLPath('/pet').params // undefined
481
- * ```
482
- */
483
- get params() {
484
- return this.getParams();
485
- }
486
- #transformParam(raw) {
487
- const param = isValidVarName(raw) ? raw : camelCase(raw);
488
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
489
- }
490
- /**
491
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
509
+ * Url.toObject('/pet/{petId}')
510
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
492
511
  */
493
- #eachParam(fn) {
494
- for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
495
- const raw = match[1];
496
- fn(raw, this.#transformParam(raw));
497
- }
498
- }
499
- toObject({ type = "path", replacer, stringify } = {}) {
512
+ static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
500
513
  const object = {
501
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
502
- params: this.getParams()
514
+ url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
515
+ replacer,
516
+ casing
517
+ }),
518
+ params: toParamsObject(path, {
519
+ replacer,
520
+ casing
521
+ })
503
522
  };
504
523
  if (stringify) {
505
524
  if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
@@ -508,50 +527,6 @@ var URLPath = class {
508
527
  }
509
528
  return object;
510
529
  }
511
- /**
512
- * Converts the OpenAPI path to a TypeScript template literal string.
513
- * An optional `replacer` can transform each extracted parameter name before interpolation.
514
- *
515
- * @example
516
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
517
- */
518
- toTemplateString({ prefix, replacer } = {}) {
519
- const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
520
- if (i % 2 === 0) return part;
521
- const param = this.#transformParam(part);
522
- return `\${${replacer ? replacer(param) : param}}`;
523
- }).join("");
524
- return `\`${prefix ?? ""}${result}\``;
525
- }
526
- /**
527
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
528
- * An optional `replacer` transforms each parameter name in both key and value positions.
529
- * Returns `undefined` when no path parameters are found.
530
- *
531
- * @example
532
- * ```ts
533
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
534
- * // { petId: 'petId', tagId: 'tagId' }
535
- * ```
536
- */
537
- getParams(replacer) {
538
- const params = {};
539
- this.#eachParam((_raw, param) => {
540
- const key = replacer ? replacer(param) : param;
541
- params[key] = key;
542
- });
543
- return Object.keys(params).length > 0 ? params : void 0;
544
- }
545
- /** Converts the OpenAPI path to Express-style colon syntax.
546
- *
547
- * @example
548
- * ```ts
549
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
550
- * ```
551
- */
552
- toURLPath() {
553
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
554
- }
555
530
  };
556
531
  //#endregion
557
532
  //#region src/bundler.ts
@@ -568,7 +543,7 @@ async function readSource(sourcePath) {
568
543
  async function resolveSource(sourcePath) {
569
544
  const data = await readSource(sourcePath);
570
545
  if (sourcePath.toLowerCase().endsWith(".md")) return data;
571
- return yaml.load(data);
546
+ return parse(data);
572
547
  }
573
548
  /**
574
549
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
@@ -673,15 +648,9 @@ function isDiscriminator(obj) {
673
648
  * const document = await parse(rawDocumentObject, { canBundle: false })
674
649
  * ```
675
650
  */
676
- async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true } = {}) {
677
- if (typeof pathOrApi === "string" && canBundle) return parseDocument(await bundleDocument(pathOrApi), {
678
- canBundle: false,
679
- enablePaths
680
- });
681
- const document = await new OASNormalize(pathOrApi, {
682
- enablePaths,
683
- colorizeErrors: true
684
- }).load();
651
+ async function parseDocument(pathOrApi, { canBundle = true } = {}) {
652
+ if (typeof pathOrApi === "string" && canBundle) return parseDocument(await bundleDocument(pathOrApi), { canBundle: false });
653
+ const document = typeof pathOrApi === "string" ? parse(pathOrApi) : pathOrApi;
685
654
  if (isOpenApiV2Document(document)) {
686
655
  const { default: swagger2openapi } = await import("swagger2openapi");
687
656
  const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
@@ -692,7 +661,7 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
692
661
  /**
693
662
  * Deep-merges multiple OpenAPI documents into a single `Document`.
694
663
  *
695
- * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
664
+ * Each document is parsed independently, then deep-merged into one in array order.
696
665
  * Throws when the input array is empty.
697
666
  *
698
667
  * @example
@@ -701,10 +670,7 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
701
670
  * ```
702
671
  */
703
672
  async function mergeDocuments(pathOrApi) {
704
- const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, {
705
- enablePaths: false,
706
- canBundle: false
707
- })));
673
+ const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })));
708
674
  if (documents.length === 0) throw new Diagnostics.Error({
709
675
  code: Diagnostics.code.inputRequired,
710
676
  severity: "error",
@@ -743,7 +709,7 @@ async function parseFromConfig(source) {
743
709
  return parseDocument(source.data, { canBundle: false });
744
710
  }
745
711
  if (source.type === "paths") return mergeDocuments(source.paths);
746
- if (new URLPath(source.path).isURL) return parseDocument(source.path);
712
+ if (Url.canParse(source.path)) return parseDocument(source.path);
747
713
  const resolved = path.resolve(path.dirname(source.path), source.path);
748
714
  await assertInputExists(resolved);
749
715
  return parseDocument(resolved);
@@ -754,7 +720,7 @@ async function parseFromConfig(source) {
754
720
  * its parse error instead.
755
721
  */
756
722
  async function assertInputExists(input) {
757
- if (new URLPath(input).isURL) return;
723
+ if (Url.canParse(input)) return;
758
724
  if (!await exists(input)) throw new Diagnostics.Error({
759
725
  code: Diagnostics.code.inputNotFound,
760
726
  severity: "error",
@@ -764,7 +730,7 @@ async function assertInputExists(input) {
764
730
  });
765
731
  }
766
732
  /**
767
- * Validates an OpenAPI document using `oas-normalize` with colorized error output.
733
+ * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
768
734
  *
769
735
  * @example
770
736
  * ```ts
@@ -773,10 +739,8 @@ async function assertInputExists(input) {
773
739
  */
774
740
  async function validateDocument(document, { throwOnError = false } = {}) {
775
741
  try {
776
- await new OASNormalize(document, {
777
- enablePaths: true,
778
- colorizeErrors: true
779
- }).validate({ parser: { validate: { errors: { colorize: true } } } });
742
+ const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
743
+ if (!result.valid) throw new Error(compileErrors(result));
780
744
  } catch (error) {
781
745
  if (throwOnError) throw error;
782
746
  }
@@ -876,6 +840,161 @@ const oasDialect = ast.defineSchemaDialect({
876
840
  resolveRef
877
841
  });
878
842
  //#endregion
843
+ //#region src/mime.ts
844
+ /**
845
+ * MIME type fragments that mark a media type as JSON-like.
846
+ *
847
+ * Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
848
+ * JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
849
+ * `application/vnd.api+json`.
850
+ */
851
+ const jsonMimeFragments = [
852
+ "application/json",
853
+ "application/x-json",
854
+ "text/json",
855
+ "text/x-json",
856
+ "+json"
857
+ ];
858
+ /**
859
+ * Returns `true` when a media type string is JSON-like.
860
+ *
861
+ * @example
862
+ * ```ts
863
+ * isJsonMimeType('application/json') // true
864
+ * isJsonMimeType('application/vnd.api+json') // true
865
+ * isJsonMimeType('multipart/form-data') // false
866
+ * ```
867
+ */
868
+ function isJsonMimeType(mimeType) {
869
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
870
+ }
871
+ //#endregion
872
+ //#region src/operation.ts
873
+ /**
874
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
875
+ * with no leading or trailing dash.
876
+ */
877
+ function slugify(value) {
878
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
879
+ }
880
+ /**
881
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
882
+ */
883
+ function getOperationId({ path, method, schema }) {
884
+ const { operationId } = schema;
885
+ if (typeof operationId === "string" && operationId.length > 0) return operationId;
886
+ return `${method}_${slugify(path).toLowerCase()}`;
887
+ }
888
+ /**
889
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
890
+ */
891
+ function getResponseStatusCodes({ schema }) {
892
+ const responses = schema.responses;
893
+ if (!responses || isReference(responses)) return [];
894
+ return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
895
+ }
896
+ /**
897
+ * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
898
+ */
899
+ function getResponseByStatusCode({ document, operation, statusCode }) {
900
+ const responses = operation.schema.responses;
901
+ if (!responses || isReference(responses)) return false;
902
+ const response = responses[statusCode];
903
+ if (!response) return false;
904
+ if (isReference(response)) {
905
+ const resolved = resolveRef(document, response.$ref);
906
+ responses[statusCode] = resolved;
907
+ if (!resolved || isReference(resolved)) return false;
908
+ return resolved;
909
+ }
910
+ return response;
911
+ }
912
+ /**
913
+ * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
914
+ * `undefined` when the operation has no request body.
915
+ */
916
+ function getRequestBodyContent({ document, operation }) {
917
+ const { schema } = operation;
918
+ let requestBody = schema.requestBody;
919
+ if (!requestBody) return;
920
+ if (isReference(requestBody)) {
921
+ const resolved = resolveRef(document, requestBody.$ref);
922
+ schema.requestBody = resolved;
923
+ if (!resolved || isReference(resolved)) return;
924
+ requestBody = resolved;
925
+ }
926
+ return requestBody.content;
927
+ }
928
+ /**
929
+ * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
930
+ * Otherwise picks the first JSON-like media type, then the first declared one, as a
931
+ * `[mediaType, object]` tuple.
932
+ */
933
+ function getRequestContent({ document, operation, mediaType }) {
934
+ const content = getRequestBodyContent({
935
+ document,
936
+ operation
937
+ });
938
+ if (!content) return false;
939
+ if (mediaType) return mediaType in content ? content[mediaType] : false;
940
+ const mediaTypes = Object.keys(content);
941
+ const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
942
+ return available ? [available, content[available]] : false;
943
+ }
944
+ /**
945
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
946
+ * matching the previous behavior), then the first declared one, defaulting to `'application/json'`.
947
+ */
948
+ function getRequestContentType({ document, operation }) {
949
+ const content = getRequestBodyContent({
950
+ document,
951
+ operation
952
+ });
953
+ const mediaTypes = content ? Object.keys(content) : [];
954
+ let result = mediaTypes[0] ?? "application/json";
955
+ for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
956
+ return result;
957
+ }
958
+ /**
959
+ * Builds an `Operation` for every supported HTTP method on every path, in document order.
960
+ * `x-` path keys and unresolvable path-item `$ref`s are skipped.
961
+ *
962
+ * @example
963
+ * ```ts
964
+ * for (const operation of getOperations(document)) {
965
+ * parseOperation(options, operation)
966
+ * }
967
+ * ```
968
+ */
969
+ function getOperations(document) {
970
+ const operations = [];
971
+ const paths = document.paths;
972
+ if (!paths) return operations;
973
+ for (const path of Object.keys(paths)) {
974
+ if (path.startsWith("x-")) continue;
975
+ let pathItem = paths[path];
976
+ if (!pathItem) continue;
977
+ if (isReference(pathItem)) {
978
+ const resolved = resolveRef(document, pathItem.$ref);
979
+ paths[path] = resolved;
980
+ if (!resolved || isReference(resolved)) continue;
981
+ pathItem = resolved;
982
+ }
983
+ const item = pathItem;
984
+ for (const method of Object.keys(item)) {
985
+ if (!SUPPORTED_METHODS.has(method)) continue;
986
+ const schema = item[method];
987
+ if (!schema || typeof schema !== "object") continue;
988
+ operations.push({
989
+ path,
990
+ method,
991
+ schema
992
+ });
993
+ }
994
+ }
995
+ return operations;
996
+ }
997
+ //#endregion
879
998
  //#region src/resolvers.ts
880
999
  /**
881
1000
  * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
@@ -968,7 +1087,7 @@ function getResponseBody(responseBody, contentType) {
968
1087
  }
969
1088
  let availableContentType;
970
1089
  const contentTypes = Object.keys(body.content);
971
- for (const mt of contentTypes) if (matchesMimeType.json(mt)) {
1090
+ for (const mt of contentTypes) if (isJsonMimeType(mt)) {
972
1091
  availableContentType = mt;
973
1092
  break;
974
1093
  }
@@ -999,7 +1118,11 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
999
1118
  if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
1000
1119
  }
1001
1120
  }
1002
- const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
1121
+ const responseBody = getResponseBody(getResponseByStatusCode({
1122
+ document,
1123
+ operation,
1124
+ statusCode
1125
+ }), options.contentType);
1003
1126
  if (responseBody === false) return {};
1004
1127
  const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
1005
1128
  if (!schema) return {};
@@ -1015,7 +1138,11 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
1015
1138
  */
1016
1139
  function getRequestSchema(document, operation, options = {}) {
1017
1140
  if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
1018
- const requestBody = operation.getRequestBody(options.contentType);
1141
+ const requestBody = getRequestContent({
1142
+ document,
1143
+ operation,
1144
+ mediaType: options.contentType
1145
+ });
1019
1146
  if (requestBody === false) return null;
1020
1147
  const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
1021
1148
  if (!schema) return null;
@@ -1050,7 +1177,7 @@ function hasStructuralKeywords(fragment) {
1050
1177
  function flattenSchema(schema) {
1051
1178
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
1052
1179
  const allOfFragments = schema.allOf;
1053
- if (allOfFragments.some((item) => isRef(item))) return schema;
1180
+ if (allOfFragments.some((item) => isReference(item))) return schema;
1054
1181
  if (allOfFragments.some(hasStructuralKeywords)) return schema;
1055
1182
  const merged = { ...schema };
1056
1183
  delete merged.allOf;
@@ -1288,7 +1415,11 @@ function getResponseBodyContentTypes(document, operation, statusCode) {
1288
1415
  if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
1289
1416
  }
1290
1417
  }
1291
- const responseObj = operation.getResponseByStatusCode(statusCode);
1418
+ const responseObj = getResponseByStatusCode({
1419
+ document,
1420
+ operation,
1421
+ statusCode
1422
+ });
1292
1423
  if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1293
1424
  const body = responseObj;
1294
1425
  return body.content ? Object.keys(body.content) : [];
@@ -1872,7 +2003,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1872
2003
  });
1873
2004
  }
1874
2005
  /**
1875
- * Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
2006
+ * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
1876
2007
  * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1877
2008
  * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1878
2009
  * match/convert/fall-through contract.
@@ -1970,10 +2101,10 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1970
2101
  }
1971
2102
  ];
1972
2103
  /**
1973
- * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
2104
+ * Converts an OAS `SchemaObject` into a `SchemaNode`.
1974
2105
  *
1975
- * Builds the per-schema context, then runs it through the ordered {@link schemaRules} table
1976
- * via {@link ast.dispatch}. When no rule produces a node, falls back to the configured
2106
+ * Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
2107
+ * the first converter that produces a node. When none match, falls back to the configured
1977
2108
  * `emptySchemaType`.
1978
2109
  */
1979
2110
  function parseSchema({ schema, name }, rawOptions) {
@@ -1996,8 +2127,11 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1996
2127
  rawOptions,
1997
2128
  options
1998
2129
  };
1999
- const node = ast.dispatch(schemaRules, schemaCtx);
2000
- if (node) return node;
2130
+ for (const rule of schemaRules) {
2131
+ if (!rule.match(schemaCtx)) continue;
2132
+ const node = rule.convert(schemaCtx);
2133
+ if (node) return node;
2134
+ }
2001
2135
  const emptyType = typeOptionMap.get(options.emptySchemaType);
2002
2136
  return ast.createSchema({
2003
2137
  type: emptyType,
@@ -2068,7 +2202,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2068
2202
  * Converts an OAS `Operation` into an `OperationNode`.
2069
2203
  */
2070
2204
  function parseOperation(options, operation) {
2071
- const operationId = operation.getOperationId();
2205
+ const operationId = getOperationId(operation);
2072
2206
  const operationName = operationId ? pascalCase(operationId) : void 0;
2073
2207
  const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
2074
2208
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
@@ -2091,8 +2225,12 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2091
2225
  required: requestBodyMeta.required || void 0,
2092
2226
  content: content.length > 0 ? content : void 0
2093
2227
  } : void 0;
2094
- const responses = operation.getResponseStatusCodes().map((statusCode) => {
2095
- const responseObj = operation.getResponseByStatusCode(statusCode);
2228
+ const responses = getResponseStatusCodes(operation).map((statusCode) => {
2229
+ const responseObj = getResponseByStatusCode({
2230
+ document,
2231
+ operation,
2232
+ statusCode
2233
+ });
2096
2234
  const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2097
2235
  const { description } = getResponseMeta(responseObj);
2098
2236
  const parseEntrySchema = (contentType) => {
@@ -2110,7 +2248,10 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2110
2248
  ...parseEntrySchema(contentType)
2111
2249
  }));
2112
2250
  if (content.length === 0) content.push({
2113
- contentType: operation.contentType || "application/json",
2251
+ contentType: getRequestContentType({
2252
+ document,
2253
+ operation
2254
+ }) || "application/json",
2114
2255
  ...parseEntrySchema(ctx.contentType)
2115
2256
  });
2116
2257
  return ast.createResponse({
@@ -2119,16 +2260,23 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2119
2260
  content
2120
2261
  });
2121
2262
  });
2122
- const urlPath = new URLPath(operation.path);
2263
+ const pathItem = document.paths?.[operation.path];
2264
+ const pathItemDoc = pathItem && !dialect.isReference(pathItem) ? pathItem : void 0;
2265
+ const pickDoc = (key) => {
2266
+ const own = operation.schema[key];
2267
+ if (typeof own === "string") return own;
2268
+ const fallback = pathItemDoc?.[key];
2269
+ return typeof fallback === "string" ? fallback : void 0;
2270
+ };
2123
2271
  return ast.createOperation({
2124
2272
  operationId,
2125
2273
  protocol: "http",
2126
2274
  method: operation.method.toUpperCase(),
2127
- path: urlPath.path,
2128
- tags: operation.getTags().map((tag) => tag.name),
2129
- summary: operation.getSummary() || void 0,
2130
- description: operation.getDescription() || void 0,
2131
- deprecated: operation.isDeprecated() || void 0,
2275
+ path: operation.path,
2276
+ tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
2277
+ summary: pickDoc("summary") || void 0,
2278
+ description: pickDoc("description") || void 0,
2279
+ deprecated: operation.schema.deprecated || void 0,
2132
2280
  parameters,
2133
2281
  requestBody,
2134
2282
  responses
@@ -2346,7 +2494,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2346
2494
  * })
2347
2495
  * ```
2348
2496
  */
2349
- function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe }) {
2497
+ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions, discriminator, dedupe }) {
2350
2498
  const allNodes = [];
2351
2499
  const refAliasMap = /* @__PURE__ */ new Map();
2352
2500
  const enumNames = [];
@@ -2370,8 +2518,7 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2370
2518
  let dedupePlan = null;
2371
2519
  if (dedupe) {
2372
2520
  const operationNodes = [];
2373
- for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2374
- if (!operation) continue;
2521
+ for (const operation of getOperations(document)) {
2375
2522
  const operationNode = parseOperation(parserOptions, operation);
2376
2523
  if (operationNode) operationNodes.push(operationNode);
2377
2524
  }
@@ -2383,16 +2530,17 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2383
2530
  });
2384
2531
  for (const definition of dedupePlan.hoisted) if (definition.type === "enum" && definition.name) enumNames.push(definition.name);
2385
2532
  }
2533
+ const aliasNames = dedupePlan?.aliasNames;
2386
2534
  return {
2387
2535
  refAliasMap,
2388
- enumNames,
2536
+ enumNames: aliasNames && aliasNames.size > 0 ? enumNames.filter((name) => !aliasNames.has(name)) : enumNames,
2389
2537
  circularNames,
2390
2538
  discriminatorChildMap,
2391
2539
  dedupePlan
2392
2540
  };
2393
2541
  }
2394
2542
  /**
2395
- * Creates a lazy `InputStreamNode` from already-resolved adapter state.
2543
+ * Creates a lazy `InputNode<true>` from already-resolved adapter state.
2396
2544
  *
2397
2545
  * The schema and operation iterables each start a fresh parse pass on every
2398
2546
  * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
@@ -2403,16 +2551,16 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2403
2551
  *
2404
2552
  * @example
2405
2553
  * ```ts
2406
- * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, meta })
2554
+ * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, meta })
2407
2555
  * for await (const schema of streamNode.schemas) {
2408
2556
  * // each call to for-await restarts from the first schema
2409
2557
  * }
2410
2558
  * ```
2411
2559
  */
2412
- function createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2560
+ function createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2413
2561
  const rewriteTopLevelSchema = (node) => {
2414
2562
  if (!dedupePlan) return node;
2415
- const canonical = dedupePlan.canonicalBySignature.get(ast.schemaSignature(node));
2563
+ const canonical = dedupePlan.canonicalBySignature.get(ast.signatureOf(node));
2416
2564
  if (canonical && canonical.name !== node.name) return ast.createSchema({
2417
2565
  type: "ref",
2418
2566
  name: node.name ?? null,
@@ -2420,12 +2568,13 @@ function createInputStream({ schemas, parseSchema, parseOperation, baseOas, pars
2420
2568
  description: node.description,
2421
2569
  deprecated: node.deprecated
2422
2570
  });
2423
- return ast.applyDedupe(node, dedupePlan.canonicalBySignature, true);
2571
+ return ast.applyDedupe(node, dedupePlan, true);
2424
2572
  };
2425
2573
  const schemasIterable = { [Symbol.asyncIterator]() {
2426
2574
  return (async function* () {
2427
2575
  if (dedupePlan) for (const definition of dedupePlan.hoisted) yield definition;
2428
2576
  for (const [name, schema] of Object.entries(schemas)) {
2577
+ if (dedupePlan?.aliasNames.has(name)) continue;
2429
2578
  const alias = refAliasMap.get(name);
2430
2579
  if (alias?.name && schemas[alias.name]) {
2431
2580
  yield rewriteTopLevelSchema({
@@ -2447,10 +2596,9 @@ function createInputStream({ schemas, parseSchema, parseOperation, baseOas, pars
2447
2596
  } };
2448
2597
  const operationsIterable = { [Symbol.asyncIterator]() {
2449
2598
  return (async function* () {
2450
- for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2451
- if (!operation) continue;
2599
+ for (const operation of getOperations(document)) {
2452
2600
  const node = parseOperation(parserOptions, operation);
2453
- if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan.canonicalBySignature) : node;
2601
+ if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan) : node;
2454
2602
  }
2455
2603
  })();
2456
2604
  } };
@@ -2503,7 +2651,6 @@ const adapterOas = createAdapter((options) => {
2503
2651
  let parsedDocument = null;
2504
2652
  const documentCache = /* @__PURE__ */ new WeakMap();
2505
2653
  const schemasCache = /* @__PURE__ */ new WeakMap();
2506
- const baseOasCache = /* @__PURE__ */ new WeakMap();
2507
2654
  const schemaParserCache = /* @__PURE__ */ new WeakMap();
2508
2655
  const preScanCache = /* @__PURE__ */ new WeakMap();
2509
2656
  function ensureDocument(source) {
@@ -2529,13 +2676,6 @@ const adapterOas = createAdapter((options) => {
2529
2676
  schemasCache.set(document, promise);
2530
2677
  return promise;
2531
2678
  }
2532
- function ensureBaseOas(document) {
2533
- const cached = baseOasCache.get(document);
2534
- if (cached) return cached;
2535
- const baseOas = new BaseOas(document);
2536
- baseOasCache.set(document, baseOas);
2537
- return baseOas;
2538
- }
2539
2679
  function ensureSchemaParser(document) {
2540
2680
  const cached = schemaParserCache.get(document);
2541
2681
  if (cached) return cached;
@@ -2546,14 +2686,14 @@ const adapterOas = createAdapter((options) => {
2546
2686
  schemaParserCache.set(document, parser);
2547
2687
  return parser;
2548
2688
  }
2549
- function ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas) {
2689
+ function ensurePreScan(document, schemas, parseSchema, parseOperation) {
2550
2690
  const cached = preScanCache.get(document);
2551
2691
  if (cached) return cached;
2552
2692
  const result = preScan({
2553
2693
  schemas,
2554
2694
  parseSchema,
2555
2695
  parseOperation,
2556
- baseOas,
2696
+ document,
2557
2697
  parserOptions,
2558
2698
  discriminator,
2559
2699
  dedupe
@@ -2565,13 +2705,12 @@ const adapterOas = createAdapter((options) => {
2565
2705
  const document = await ensureDocument(source);
2566
2706
  const schemas = await ensureSchemas(document);
2567
2707
  const { parseSchema, parseOperation } = ensureSchemaParser(document);
2568
- const baseOas = ensureBaseOas(document);
2569
- const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas);
2708
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation);
2570
2709
  return createInputStream({
2571
2710
  schemas,
2572
2711
  parseSchema,
2573
2712
  parseOperation,
2574
- baseOas,
2713
+ document,
2575
2714
  parserOptions,
2576
2715
  refAliasMap,
2577
2716
  discriminatorChildMap,
@@ -2616,18 +2755,17 @@ const adapterOas = createAdapter((options) => {
2616
2755
  await validateDocument(await parseDocument(input), options);
2617
2756
  },
2618
2757
  getImports(node, resolve) {
2619
- return ast.collectImports({
2620
- node,
2621
- nameMapping,
2622
- resolve: (schemaName) => {
2623
- const result = resolve(schemaName);
2624
- if (!result) return null;
2625
- return ast.createImport({
2626
- name: [result.name],
2627
- path: result.path
2628
- });
2629
- }
2630
- });
2758
+ return collect(node, { schema(schemaNode) {
2759
+ const schemaRef = narrowSchema(schemaNode, "ref");
2760
+ if (!schemaRef?.ref) return null;
2761
+ const rawName = extractRefName(schemaRef.ref);
2762
+ const result = resolve(nameMapping.get(rawName) ?? rawName);
2763
+ if (!result) return null;
2764
+ return ast.createImport({
2765
+ name: [result.name],
2766
+ path: result.path
2767
+ });
2768
+ } });
2631
2769
  },
2632
2770
  async parse(source) {
2633
2771
  const streamNode = await createStream(source);