@kubb/oas
Version:
OpenAPI Specification (OAS) utilities and helpers for Kubb, providing parsing, normalization, and manipulation of OpenAPI/Swagger schemas.
968 lines (967 loc) • 35.1 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let jsonpointer = require("jsonpointer");
jsonpointer = __toESM(jsonpointer);
let oas = require("oas");
oas = __toESM(oas);
let oas_utils = require("oas/utils");
let node_path = require("node:path");
node_path = __toESM(node_path);
let _redocly_openapi_core = require("@redocly/openapi-core");
let _stoplight_yaml = require("@stoplight/yaml");
_stoplight_yaml = __toESM(_stoplight_yaml);
let oas_types = require("oas/types");
let oas_normalize = require("oas-normalize");
oas_normalize = __toESM(oas_normalize);
let remeda = require("remeda");
let swagger2openapi = require("swagger2openapi");
swagger2openapi = __toESM(swagger2openapi);
//#region src/constants.ts
/**
* JSON Schema keywords that indicate structural composition.
* Used when deciding whether an inline `allOf` fragment can be safely flattened
* into its parent (fragments containing any of these keys must not be inlined).
*/
const STRUCTURAL_KEYS = new Set([
"properties",
"items",
"additionalProperties",
"oneOf",
"anyOf",
"allOf",
"not"
]);
/**
* Maps OAS/JSON Schema `format` strings to their Kubb `SchemaType` equivalents.
*
* Only formats that require a type different from the raw OAS `type` are listed here.
* `int64`, `date-time`, `date`, and `time` are handled separately because their
* output depends on runtime parser options and cannot live in a static map.
*
* Note: `ipv4`, `ipv6`, and `hostname` map to `'url'` — not semantically accurate,
* but `'url'` is the closest supported scalar type in the Kubb AST.
*/
const FORMAT_MAP = {
uuid: "uuid",
email: "email",
"idn-email": "email",
uri: "url",
"uri-reference": "url",
url: "url",
ipv4: "url",
ipv6: "url",
hostname: "url",
"idn-hostname": "url",
binary: "blob",
byte: "blob",
int32: "integer",
float: "number",
double: "number"
};
/**
* Exhaustive list of media types that Kubb recognizes.
* Kept as a module-level constant to avoid re-allocating the array on every call.
*/
const KNOWN_MEDIA_TYPES = [
"application/json",
"application/xml",
"application/x-www-form-urlencoded",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/graphql",
"multipart/form-data",
"text/plain",
"text/html",
"text/csv",
"text/xml",
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"image/svg+xml",
"audio/mpeg",
"video/mp4"
];
/**
* Vendor extension keys used by various spec generators to attach human-readable
* labels to enum values. Checked in priority order: the first key found wins.
*/
const ENUM_EXTENSION_KEYS = ["x-enumNames", "x-enum-varnames"];
/**
* Canonical HTTP method names used throughout the Kubb OAS layer.
* Keys are uppercase (as used in generated code); values are the lowercase
* strings that the `oas` library uses internally.
* @deprecated use httpMethods from @kubb/ast
*/
const httpMethods = {
GET: "get",
POST: "post",
PUT: "put",
PATCH: "patch",
DELETE: "delete",
HEAD: "head",
OPTIONS: "options",
TRACE: "trace"
};
//#endregion
//#region ../../internals/utils/src/casing.ts
/**
* Shared implementation for camelCase and PascalCase conversion.
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
* and capitalizes each word according to `pascal`.
*
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
*/
function toCamelOrPascal(text, pascal) {
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) => {
if (word.length > 1 && word === word.toUpperCase()) return word;
if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
return word.charAt(0).toUpperCase() + word.slice(1);
}).join("").replace(/[^a-zA-Z0-9]/g, "");
}
/**
* Splits `text` on `.` and applies `transformPart` to each segment.
* The last segment receives `isLast = true`, all earlier segments receive `false`.
* Segments are joined with `/` to form a file path.
*/
function applyToFileParts(text, transformPart) {
const parts = text.split(".");
return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
}
/**
* Converts `text` to camelCase.
* When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
*
* @example
* camelCase('hello-world') // 'helloWorld'
* camelCase('pet.petId', { isFile: true }) // 'pet/petId'
*/
function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
prefix,
suffix
} : {}));
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
}
/**
* Converts `text` to PascalCase.
* When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
*
* @example
* pascalCase('hello-world') // 'HelloWorld'
* pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
*/
function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
prefix,
suffix
}) : camelCase(part));
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
}
//#endregion
//#region ../../internals/utils/src/reserved.ts
/**
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
*/
function isValidVarName(name) {
try {
new Function(`var ${name}`);
} catch {
return false;
}
return true;
}
//#endregion
//#region ../../internals/utils/src/urlPath.ts
/**
* Parses and transforms an OpenAPI/Swagger path string into various URL formats.
*
* @example
* const p = new URLPath('/pet/{petId}')
* p.URL // '/pet/:petId'
* p.template // '`/pet/${petId}`'
*/
var URLPath = class {
/** The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`. */
path;
#options;
constructor(path, options = {}) {
this.path = path;
this.#options = options;
}
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`. */
get URL() {
return this.toURLPath();
}
/** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`). */
get isURL() {
try {
return !!new URL(this.path).href;
} catch {
return false;
}
}
/**
* Converts the OpenAPI path to a TypeScript template literal string.
*
* @example
* new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
* new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
*/
get template() {
return this.toTemplateString();
}
/** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set. */
get object() {
return this.toObject();
}
/** Returns a map of path parameter names, or `undefined` when the path has no parameters. */
get params() {
return this.getParams();
}
#transformParam(raw) {
const param = isValidVarName(raw) ? raw : camelCase(raw);
return this.#options.casing === "camelcase" ? camelCase(param) : param;
}
/** Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name. */
#eachParam(fn) {
for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
const raw = match[1];
fn(raw, this.#transformParam(raw));
}
}
toObject({ type = "path", replacer, stringify } = {}) {
const object = {
url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
params: this.getParams()
};
if (stringify) {
if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
return `{ url: '${object.url}' }`;
}
return object;
}
/**
* Converts the OpenAPI path to a TypeScript template literal string.
* An optional `replacer` can transform each extracted parameter name before interpolation.
*
* @example
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
*/
toTemplateString({ prefix = "", replacer } = {}) {
return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
if (i % 2 === 0) return part;
const param = this.#transformParam(part);
return `\${${replacer ? replacer(param) : param}}`;
}).join("")}\``;
}
/**
* Extracts all `{param}` segments from the path and returns them as a key-value map.
* An optional `replacer` transforms each parameter name in both key and value positions.
* Returns `undefined` when no path parameters are found.
*/
getParams(replacer) {
const params = {};
this.#eachParam((_raw, param) => {
const key = replacer ? replacer(param) : param;
params[key] = key;
});
return Object.keys(params).length > 0 ? params : void 0;
}
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`. */
toURLPath() {
return this.path.replace(/\{([^}]+)\}/g, ":$1");
}
};
//#endregion
//#region src/utils.ts
/**
* Returns `true` when `doc` looks like a Swagger 2.0 document (no `openapi` key).
*/
function isOpenApiV2Document(doc) {
return !!doc && (0, remeda.isPlainObject)(doc) && !("openapi" in doc);
}
/**
* Returns `true` when `doc` is an OpenAPI 3.1 document.
*/
function isOpenApiV3_1Document(doc) {
return !!doc && (0, remeda.isPlainObject)(doc) && "openapi" in doc && doc.openapi.startsWith("3.1");
}
/**
* Returns `true` when `obj` is a parameter object (has an `in` field distinguishing it from a schema).
*/
function isParameterObject(obj) {
return !!obj && "in" in obj;
}
/**
* Determines if a schema is nullable, considering:
* - OpenAPI 3.0 `nullable` / `x-nullable`
* - OpenAPI 3.1 JSON Schema `type: ['null', ...]` or `type: 'null'`
*/
function isNullable(schema) {
if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
const schemaType = schema?.type;
if (schemaType === "null") return true;
if (Array.isArray(schemaType)) return schemaType.includes("null");
return false;
}
/**
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
*/
function isReference(obj) {
return !!obj && (0, oas_types.isRef)(obj);
}
/**
* Returns `true` when `obj` is a schema that carries a structured `discriminator` object
* (as opposed to a plain string discriminator used in some older specs).
*/
function isDiscriminator(obj) {
const record = obj;
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
}
/**
* Determines whether a schema is required.
*
* Returns true if the schema has a non-empty {@link SchemaObject.required} array or a truthy {@link SchemaObject.required} property.
*/
function isRequired(schema) {
if (!schema) return false;
return Array.isArray(schema.required) ? !!schema.required?.length : !!schema.required;
}
function isAllOptional(schema) {
if (!schema) return true;
if (isOptional(schema)) return true;
const s = schema;
if (Array.isArray(s?.required) && s?.required.length > 0) return false;
const groups = [
s?.allOf,
s?.anyOf,
s?.oneOf
].filter((g) => Array.isArray(g));
if (groups.length === 0) return true;
return groups.every((arr) => arr.every((child) => isAllOptional(child)));
}
function isOptional(schema) {
return !isRequired(schema);
}
/**
* Determines the appropriate default value for a schema parameter.
* - For array types: returns '[]'
* - For union types (anyOf/oneOf):
* - If at least one variant has all-optional fields: returns '{}'
* - Otherwise: returns undefined (no default)
* - For object types with optional fields: returns '{}'
* - For primitive types (string, number, boolean): returns undefined (no default)
* - For required types: returns undefined (no default)
*/
function getDefaultValue(schema) {
if (!schema || !isOptional(schema)) return;
if (schema.type === "array") return "[]";
if (schema.anyOf || schema.oneOf) {
const variants = schema.anyOf || schema.oneOf;
if (!Array.isArray(variants)) return;
if (!variants.some((variant) => isAllOptional(variant))) return;
return "{}";
}
if (schema.type === "object" || schema.properties) return "{}";
}
async function parse(pathOrApi, { oasClass = Oas, canBundle = true, enablePaths = true } = {}) {
if (typeof pathOrApi === "string" && canBundle) return parse((await (0, _redocly_openapi_core.bundle)({
ref: pathOrApi,
config: await (0, _redocly_openapi_core.loadConfig)(),
base: pathOrApi
})).bundle.parsed, {
oasClass,
canBundle,
enablePaths
});
const document = await new oas_normalize.default(pathOrApi, {
enablePaths,
colorizeErrors: true
}).load();
if (isOpenApiV2Document(document)) {
const { openapi } = await swagger2openapi.default.convertObj(document, { anchors: true });
return new oasClass(openapi);
}
return new oasClass(document);
}
async function merge(pathOrApi, { oasClass = Oas } = {}) {
const instances = await Promise.all(pathOrApi.map((p) => parse(p, {
oasClass,
enablePaths: false,
canBundle: false
})));
if (instances.length === 0) throw new Error("No OAS instances provided for merging.");
return parse(instances.reduce((acc, current) => {
return (0, remeda.mergeDeep)(acc, current.document);
}, {
openapi: "3.0.0",
info: {
title: "Merged API",
version: "1.0.0"
},
paths: {},
components: { schemas: {} }
}), { oasClass });
}
function parseFromConfig(config, oasClass = Oas) {
if ("data" in config.input) {
if (typeof config.input.data === "object") return parse(structuredClone(config.input.data), { oasClass });
try {
return parse(_stoplight_yaml.default.parse(config.input.data), { oasClass });
} catch (_e) {
return parse(config.input.data, { oasClass });
}
}
if (Array.isArray(config.input)) return merge(config.input.map((input) => node_path.default.resolve(config.root, input.path)), { oasClass });
if (new URLPath(config.input.path).isURL) return parse(config.input.path, { oasClass });
return parse(node_path.default.resolve(config.root, config.input.path), { oasClass });
}
/**
* Flatten allOf schemas by merging keyword-only fragments.
* Only flattens schemas where allOf items don't contain structural keys or $refs.
*/
function flattenSchema(schema) {
if (!schema?.allOf || schema.allOf.length === 0) return schema || null;
if (schema.allOf.some((item) => (0, oas_types.isRef)(item))) return schema;
const isPlainFragment = (item) => !Object.keys(item).some((key) => STRUCTURAL_KEYS.has(key));
if (!schema.allOf.every((item) => isPlainFragment(item))) return schema;
const merged = { ...schema };
delete merged.allOf;
for (const fragment of schema.allOf) for (const [key, value] of Object.entries(fragment)) if (merged[key] === void 0) merged[key] = value;
return merged;
}
/**
* Validate an OpenAPI document using oas-normalize.
*/
async function validate(document) {
return new oas_normalize.default(document, {
enablePaths: true,
colorizeErrors: true
}).validate({ parser: { validate: { errors: { colorize: true } } } });
}
/**
* Collect all schema $ref dependencies recursively.
*/
function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
if (Array.isArray(schema)) {
for (const item of schema) collectRefs(item, refs);
return refs;
}
if (schema && typeof schema === "object") for (const [key, value] of Object.entries(schema)) if (key === "$ref" && typeof value === "string") {
const match = value.match(/^#\/components\/schemas\/(.+)$/);
if (match) refs.add(match[1]);
} else collectRefs(value, refs);
return refs;
}
/**
* Sort schemas topologically so referenced schemas appear first.
*/
function sortSchemas(schemas) {
const deps = /* @__PURE__ */ new Map();
for (const [name, schema] of Object.entries(schemas)) deps.set(name, Array.from(collectRefs(schema)));
const sorted = [];
const visited = /* @__PURE__ */ new Set();
function visit(name, stack = /* @__PURE__ */ new Set()) {
if (visited.has(name)) return;
if (stack.has(name)) return;
stack.add(name);
const children = deps.get(name) || [];
for (const child of children) if (deps.has(child)) visit(child, stack);
stack.delete(name);
visited.add(name);
sorted.push(name);
}
for (const name of Object.keys(schemas)) visit(name);
const sortedSchemas = {};
for (const name of sorted) sortedSchemas[name] = schemas[name];
return sortedSchemas;
}
/**
* Extract schema from content object (used by responses and requestBodies).
* Returns null if the schema is just a $ref (not a unique type definition).
*/
function extractSchemaFromContent(content, preferredContentType) {
if (!content) return null;
const firstContentType = Object.keys(content)[0] || "application/json";
const schema = content[preferredContentType || firstContentType]?.schema;
if (schema && "$ref" in schema) return null;
return schema || null;
}
/**
* Get semantic suffix for a schema source.
*/
function getSemanticSuffix(source) {
switch (source) {
case "schemas": return "Schema";
case "responses": return "Response";
case "requestBodies": return "Request";
}
}
/**
* Legacy resolution strategy - no collision detection, just use original names.
* This preserves backward compatibility when collisionDetection is false.
* @deprecated
*/
function legacyResolve(schemasWithMeta) {
const schemas = {};
const nameMapping = /* @__PURE__ */ new Map();
for (const item of schemasWithMeta) {
schemas[item.originalName] = item.schema;
const refPath = `#/components/${item.source}/${item.originalName}`;
nameMapping.set(refPath, item.originalName);
}
return {
schemas,
nameMapping
};
}
/**
* Resolve name collisions by applying suffixes based on collision type.
*
* Strategy:
* - Same-component collisions (e.g., "Variant" + "variant" both in schemas): numeric suffixes (Variant, Variant2)
* - Cross-component collisions (e.g., "Pet" in schemas + "Pet" in requestBodies): semantic suffixes (PetSchema, PetRequest)
*/
function resolveCollisions(schemasWithMeta) {
const schemas = {};
const nameMapping = /* @__PURE__ */ new Map();
const normalizedNames = /* @__PURE__ */ new Map();
for (const item of schemasWithMeta) {
const normalized = pascalCase(item.originalName);
if (!normalizedNames.has(normalized)) normalizedNames.set(normalized, []);
normalizedNames.get(normalized).push(item);
}
for (const [, items] of normalizedNames) {
if (items.length === 1) {
const item = items[0];
schemas[item.originalName] = item.schema;
const refPath = `#/components/${item.source}/${item.originalName}`;
nameMapping.set(refPath, item.originalName);
continue;
}
if (new Set(items.map((item) => item.source)).size === 1) items.forEach((item, index) => {
const suffix = index === 0 ? "" : (index + 1).toString();
const uniqueName = item.originalName + suffix;
schemas[uniqueName] = item.schema;
const refPath = `#/components/${item.source}/${item.originalName}`;
nameMapping.set(refPath, uniqueName);
});
else items.forEach((item) => {
const suffix = getSemanticSuffix(item.source);
const uniqueName = item.originalName + suffix;
schemas[uniqueName] = item.schema;
const refPath = `#/components/${item.source}/${item.originalName}`;
nameMapping.set(refPath, uniqueName);
});
}
return {
schemas,
nameMapping
};
}
//#endregion
//#region src/Oas.ts
/**
* Prefix used to create synthetic `$ref` values for anonymous (inline) discriminator schemas.
* The suffix is the schema index within the discriminator's `oneOf`/`anyOf` array.
* @example `#kubb-inline-0`
*/
const KUBB_INLINE_REF_PREFIX = "#kubb-inline-";
var Oas = class extends oas.default {
#options = { discriminator: "strict" };
document;
constructor(document) {
super(document, void 0);
this.document = document;
}
setOptions(options) {
this.#options = {
...this.#options,
...options
};
if (this.#options.discriminator === "inherit") this.#applyDiscriminatorInheritance();
}
get options() {
return this.#options;
}
get($ref) {
const origRef = $ref;
$ref = $ref.trim();
if ($ref === "") return null;
if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
else return null;
const current = jsonpointer.default.get(this.api, $ref);
if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
return current;
}
getKey($ref) {
const key = $ref.split("/").pop();
return key === "" ? void 0 : key;
}
set($ref, value) {
$ref = $ref.trim();
if ($ref === "") return false;
if ($ref.startsWith("#")) {
$ref = globalThis.decodeURIComponent($ref.substring(1));
jsonpointer.default.set(this.api, $ref, value);
}
}
#setDiscriminator(schema) {
const { mapping = {}, propertyName } = schema.discriminator;
if (this.#options.discriminator === "inherit") Object.entries(mapping).forEach(([mappingKey, mappingValue]) => {
if (mappingValue) {
const childSchema = this.get(mappingValue);
if (!childSchema) return;
if (!childSchema.properties) childSchema.properties = {};
const property = childSchema.properties[propertyName];
if (childSchema.properties) {
childSchema.properties[propertyName] = {
...childSchema.properties ? childSchema.properties[propertyName] : {},
enum: [...property?.enum?.filter((value) => value !== mappingKey) ?? [], mappingKey]
};
childSchema.required = typeof childSchema.required === "boolean" ? childSchema.required : [...new Set([...childSchema.required ?? [], propertyName])];
this.set(mappingValue, childSchema);
}
}
});
}
getDiscriminator(schema) {
if (!isDiscriminator(schema) || !schema) return null;
const { mapping = {}, propertyName } = schema.discriminator;
/**
* Helper to extract discriminator value from a schema.
* Checks in order:
* 1. Extension property matching propertyName (e.g., x-linode-ref-name)
* 2. Property with const value
* 3. Property with single enum value
* 4. Title as fallback
*/
const getDiscriminatorValue = (schema) => {
if (!schema) return null;
if (propertyName.startsWith("x-")) {
const extensionValue = schema[propertyName];
if (extensionValue && typeof extensionValue === "string") return extensionValue;
}
const propertySchema = schema.properties?.[propertyName];
if (propertySchema && "const" in propertySchema && propertySchema.const !== void 0) return String(propertySchema.const);
if (propertySchema && propertySchema.enum?.length === 1) return String(propertySchema.enum[0]);
return schema.title || null;
};
/**
* Process oneOf/anyOf items to build mapping.
* Handles both $ref and inline schemas.
*/
const processSchemas = (schemas, existingMapping) => {
schemas.forEach((schemaItem, index) => {
if (isReference(schemaItem)) {
const key = this.getKey(schemaItem.$ref);
try {
const discriminatorValue = getDiscriminatorValue(this.get(schemaItem.$ref));
const canAdd = key && !Object.values(existingMapping).includes(schemaItem.$ref);
if (canAdd && discriminatorValue) existingMapping[discriminatorValue] = schemaItem.$ref;
else if (canAdd) existingMapping[key] = schemaItem.$ref;
} catch (_error) {
if (key && !Object.values(existingMapping).includes(schemaItem.$ref)) existingMapping[key] = schemaItem.$ref;
}
} else {
const discriminatorValue = getDiscriminatorValue(schemaItem);
if (discriminatorValue) existingMapping[discriminatorValue] = `${KUBB_INLINE_REF_PREFIX}${index}`;
}
});
};
if (schema.oneOf) processSchemas(schema.oneOf, mapping);
if (schema.anyOf) processSchemas(schema.anyOf, mapping);
return {
...schema.discriminator,
mapping
};
}
dereferenceWithRef(schema) {
if (isReference(schema)) return {
...schema,
...this.get(schema.$ref),
$ref: schema.$ref
};
return schema;
}
#applyDiscriminatorInheritance() {
const components = this.api.components;
if (!components?.schemas) return;
const visited = /* @__PURE__ */ new WeakSet();
const enqueue = (value) => {
if (!value) return;
if (Array.isArray(value)) {
for (const item of value) enqueue(item);
return;
}
if (typeof value === "object") visit(value);
};
const visit = (schema) => {
if (!schema || typeof schema !== "object") return;
if (isReference(schema)) {
visit(this.get(schema.$ref));
return;
}
const schemaObject = schema;
if (visited.has(schemaObject)) return;
visited.add(schemaObject);
if (isDiscriminator(schemaObject)) this.#setDiscriminator(schemaObject);
if ("allOf" in schemaObject) enqueue(schemaObject.allOf);
if ("oneOf" in schemaObject) enqueue(schemaObject.oneOf);
if ("anyOf" in schemaObject) enqueue(schemaObject.anyOf);
if ("not" in schemaObject) enqueue(schemaObject.not);
if ("items" in schemaObject) enqueue(schemaObject.items);
if ("prefixItems" in schemaObject) enqueue(schemaObject.prefixItems);
if (schemaObject.properties) enqueue(Object.values(schemaObject.properties));
if (schemaObject.additionalProperties && typeof schemaObject.additionalProperties === "object") enqueue(schemaObject.additionalProperties);
};
for (const schema of Object.values(components.schemas)) visit(schema);
}
/**
* Oas does not have a getResponseBody(contentType)
*/
#getResponseBodyFactory(responseBody) {
function hasResponseBody(res = responseBody) {
return !!res;
}
return (contentType) => {
if (!hasResponseBody(responseBody)) return false;
if (isReference(responseBody)) return false;
if (!responseBody.content) return false;
if (contentType) {
if (!(contentType in responseBody.content)) return false;
return responseBody.content[contentType];
}
let availableContentType;
const contentTypes = Object.keys(responseBody.content);
contentTypes.forEach((mt) => {
if (!availableContentType && oas_utils.matchesMimeType.json(mt)) availableContentType = mt;
});
if (!availableContentType) contentTypes.forEach((mt) => {
if (!availableContentType) availableContentType = mt;
});
if (availableContentType) return [
availableContentType,
responseBody.content[availableContentType],
...responseBody.description ? [responseBody.description] : []
];
return false;
};
}
getResponseSchema(operation, statusCode) {
if (operation.schema.responses) Object.keys(operation.schema.responses).forEach((key) => {
const schema = operation.schema.responses[key];
const $ref = isReference(schema) ? schema.$ref : void 0;
if (schema && $ref) operation.schema.responses[key] = this.get($ref);
});
const getResponseBody = this.#getResponseBodyFactory(operation.getResponseByStatusCode(statusCode));
const { contentType } = this.#options;
const responseBody = getResponseBody(contentType);
if (responseBody === false) return {};
const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
if (!schema) return {};
return this.dereferenceWithRef(schema);
}
getRequestSchema(operation) {
const { contentType } = this.#options;
if (operation.schema.requestBody) operation.schema.requestBody = this.dereferenceWithRef(operation.schema.requestBody);
const requestBody = operation.getRequestBody(contentType);
if (requestBody === false) return;
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
if (!schema) return;
return this.dereferenceWithRef(schema);
}
getParametersSchema(operation, inKey) {
const { contentType = operation.getContentType() } = this.#options;
const resolveParams = (params) => params.map((p) => this.dereferenceWithRef(p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
const operationParams = resolveParams(operation.schema?.parameters || []);
const pathItem = this.api?.paths?.[operation.path];
const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
const paramMap = /* @__PURE__ */ new Map();
for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
const params = Array.from(paramMap.values()).filter((v) => v.in === inKey);
if (!params.length) return null;
return params.reduce((schema, pathParameters) => {
const property = pathParameters.content?.[contentType]?.schema ?? pathParameters.schema;
const required = typeof schema.required === "boolean" ? schema.required : [...schema.required || [], pathParameters.required ? pathParameters.name : void 0].filter(Boolean);
const getDefaultStyle = (location) => {
if (location === "query") return "form";
if (location === "path") return "simple";
return "simple";
};
const style = pathParameters.style || getDefaultStyle(inKey);
const explode = pathParameters.explode !== void 0 ? pathParameters.explode : style === "form";
if (inKey === "query" && style === "form" && explode === true && property?.type === "object" && property?.additionalProperties && !property?.properties) return {
...schema,
description: pathParameters.description || schema.description,
deprecated: schema.deprecated,
example: property.example || schema.example,
additionalProperties: property.additionalProperties
};
return {
...schema,
description: schema.description,
deprecated: schema.deprecated,
example: schema.example,
required,
properties: {
...schema.properties,
[pathParameters.name]: {
description: pathParameters.description,
...property
}
}
};
}, {
type: "object",
required: [],
properties: {}
});
}
async validate() {
return validate(this.api);
}
flattenSchema(schema) {
return flattenSchema(schema);
}
/**
* Get schemas from OpenAPI components (schemas, responses, requestBodies).
* Returns schemas in dependency order along with name mapping for collision resolution.
*/
getSchemas(options = {}) {
const contentType = options.contentType ?? this.#options.contentType;
const includes = options.includes ?? [
"schemas",
"requestBodies",
"responses"
];
const shouldResolveCollisions = options.collisionDetection ?? this.#options.collisionDetection ?? false;
const components = this.getDefinition().components;
const schemasWithMeta = [];
if (includes.includes("schemas")) {
const componentSchemas = components?.schemas || {};
for (const [name, schemaObject] of Object.entries(componentSchemas)) {
let schema = schemaObject;
if (isReference(schemaObject)) {
const resolved = this.get(schemaObject.$ref);
if (resolved && !isReference(resolved)) schema = resolved;
}
schemasWithMeta.push({
schema,
source: "schemas",
originalName: name
});
}
}
if (includes.includes("responses")) {
const responses = components?.responses || {};
for (const [name, response] of Object.entries(responses)) {
const schema = extractSchemaFromContent(response.content, contentType);
if (schema) {
let resolvedSchema = schema;
if (isReference(schema)) {
const resolved = this.get(schema.$ref);
if (resolved && !isReference(resolved)) resolvedSchema = resolved;
}
schemasWithMeta.push({
schema: resolvedSchema,
source: "responses",
originalName: name
});
}
}
}
if (includes.includes("requestBodies")) {
const requestBodies = components?.requestBodies || {};
for (const [name, request] of Object.entries(requestBodies)) {
const schema = extractSchemaFromContent(request.content, contentType);
if (schema) {
let resolvedSchema = schema;
if (isReference(schema)) {
const resolved = this.get(schema.$ref);
if (resolved && !isReference(resolved)) resolvedSchema = resolved;
}
schemasWithMeta.push({
schema: resolvedSchema,
source: "requestBodies",
originalName: name
});
}
}
}
const { schemas, nameMapping } = shouldResolveCollisions ? resolveCollisions(schemasWithMeta) : legacyResolve(schemasWithMeta);
return {
schemas: sortSchemas(schemas),
nameMapping
};
}
};
//#endregion
//#region src/resolveServerUrl.ts
/**
* Resolves an OpenAPI server URL by substituting `{variable}` placeholders
* with values from `overrides` (user-provided) or the spec-defined defaults.
*
* Throws if an override value is not in the variable's `enum` list.
*/
function resolveServerUrl(server, overrides) {
if (!server.variables) return server.url;
let url = server.url;
for (const [key, variable] of Object.entries(server.variables)) {
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
if (value === void 0) continue;
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Error(`Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`);
url = url.replaceAll(`{${key}}`, value);
}
return url;
}
//#endregion
exports.ENUM_EXTENSION_KEYS = ENUM_EXTENSION_KEYS;
exports.FORMAT_MAP = FORMAT_MAP;
exports.HttpMethods = httpMethods;
exports.KNOWN_MEDIA_TYPES = KNOWN_MEDIA_TYPES;
exports.KUBB_INLINE_REF_PREFIX = KUBB_INLINE_REF_PREFIX;
exports.Oas = Oas;
exports.STRUCTURAL_KEYS = STRUCTURAL_KEYS;
exports.flattenSchema = flattenSchema;
exports.getDefaultValue = getDefaultValue;
exports.httpMethods = httpMethods;
exports.isAllOptional = isAllOptional;
exports.isDiscriminator = isDiscriminator;
exports.isNullable = isNullable;
exports.isOpenApiV3_1Document = isOpenApiV3_1Document;
exports.isOptional = isOptional;
exports.isParameterObject = isParameterObject;
exports.isReference = isReference;
exports.isRequired = isRequired;
exports.merge = merge;
exports.parse = parse;
exports.parseFromConfig = parseFromConfig;
exports.resolveServerUrl = resolveServerUrl;
exports.validate = validate;
//# sourceMappingURL=index.cjs.map