UNPKG

@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
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