UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

247 lines (218 loc) • 7.63 kB
import { json2xml } from "./schema-utils.js"; /** * Replaces all occurrences of path parameters in a URL path template. * Example: replacePathParams('/users/{id}/details', { id: '123' }) => '/users/123/details' */ export function replacePathParams(pathTemplate = "", pathParams = {}) { let resolvedPath = pathTemplate; if (!pathParams || typeof pathParams !== "object") { return resolvedPath; } for (const [key, rawValue] of Object.entries(pathParams)) { const value = rawValue !== undefined && rawValue !== null ? String(rawValue) : ""; // Replace all occurrences of {paramName} const token = `{${key}}`; resolvedPath = resolvedPath.replaceAll(token, encodeURIComponent(value)); } return resolvedPath; } /** * A value typed into an array input is a string; in a JSON array, numbers and booleans keep their JSON type. */ function typedArrayItem(value, itemType) { if (typeof value !== "string") { return value; } // an OpenAPI 3.1 type list such as ["integer", "null"] arrives as "integer,null" const types = String(itemType).split(","); const text = value.trim(); if ( (types.includes("integer") || types.includes("number")) && text !== "" && Number.isFinite(Number(text)) ) { return Number(text); } if (types.includes("boolean") && (text === "true" || text === "false")) { return text === "true"; } return value; } /** * Serializes a single query parameter into an array of [key, value] tuples based on OpenAPI style and explode rules. */ export function serializeQueryParam(name, rawValue, options = {}) { const { style = "form", explode = style === "form", isArray = Array.isArray(rawValue), isObject = false, itemType = "", } = options; if (rawValue === undefined || rawValue === null || rawValue === "") { return []; } // Handle Object types (from JSON input or object values) if (isObject || (typeof rawValue === "object" && !Array.isArray(rawValue))) { let obj = rawValue; if (typeof rawValue === "string") { try { obj = JSON.parse(rawValue); } catch { return []; } } if (!obj || typeof obj !== "object") { return []; } if (style === "json") { return [[name, JSON.stringify(obj)]]; } if (style === "xml") { return [[name, json2xml(obj)]]; } const result = []; for (const key of Object.keys(obj)) { const pKey = `${name}[${key}]`; const val = obj[key]; if (val !== undefined && val !== null) { if (typeof val === "object" && Array.isArray(val)) { const filtered = val.filter((v) => v !== "" && v !== undefined && v !== null); if (filtered.length > 0) { if (style === "spaceDelimited") { result.push([pKey, filtered.join(" ")]); } else if (style === "pipeDelimited") { result.push([pKey, filtered.join("|")]); } else if (explode) { filtered.forEach((v) => result.push([pKey, String(v)])); } else { result.push([pKey, filtered.join(",")]); } } } else if (typeof val === "object") { result.push([pKey, JSON.stringify(val)]); } else { result.push([pKey, String(val)]); } } } return result; } // Handle Array types if (isArray || Array.isArray(rawValue)) { const rawArr = Array.isArray(rawValue) ? rawValue : [rawValue]; const vals = rawArr.filter((v) => v !== "" && v !== undefined && v !== null); if (vals.length === 0) { return []; } // A parameter described by content: application/json is one JSON array, not one parameter per value if (style === "json") { return [[name, JSON.stringify(vals.map((v) => typedArrayItem(v, itemType)))]]; } if (style === "spaceDelimited") { return [[name, vals.join(" ")]]; } if (style === "pipeDelimited") { return [[name, vals.join("|")]]; } if (explode) { return vals.map((v) => [name, String(v)]); } // Default form style unexploded: comma-delimited return [[name, vals.join(",")]]; } // Handle Primitive types return [[name, String(rawValue)]]; } /** * RFC 3986 allowable reserved characters when allowReserved: true * Encodes special delimiters & and = so query string structure remains intact, * while leaving other reserved chars (:/?#[]@!$'()*+,;) unencoded. */ function encodeReservedQueryValue(value) { return encodeURI(value).replace(/&/g, "%26").replace(/=/g, "%3D").replace(/\+/g, "%2B"); } /** * Builds the query string from a collection of query parameter descriptors. */ export function buildQueryString(queryParams = []) { if (!Array.isArray(queryParams) || queryParams.length === 0) { return ""; } const parts = []; for (const param of queryParams) { const { name, value, style = "form", explode = style === "form", allowReserved = false, isArray = false, isObject = false, itemType = "", } = param; const entries = serializeQueryParam(name, value, { style, explode, isArray, isObject, itemType, }); for (const [k, v] of entries) { const encodedKey = encodeURIComponent(k); const encodedValue = allowReserved ? encodeReservedQueryValue(v) : encodeURIComponent(v); parts.push(`${encodedKey}=${encodedValue}`); } } return parts.join("&"); } /** * The OAuth redirect URL for the `oauth-receiver` attribute. A relative value (the default * `oauth-receiver.html`) is resolved against the current page's folder, as it always was; a value starting * with `/` is a path from the site root, and a full URL is used as is, so a whole site can register one * redirect URL with its OAuth provider. * * @param {{origin: string, pathname: string}} location - e.g. window.location * @param {string} receiver - the oauth-receiver attribute * @returns {URL} */ export function oauthReceiverUrl(location, receiver = "oauth-receiver.html") { if (/^[a-z][a-z0-9+.-]*:/i.test(receiver)) return new URL(receiver); if (receiver.startsWith("/")) return new URL(receiver, location.origin); const folder = location.pathname.substring(0, location.pathname.lastIndexOf("/")); return new URL(`${location.origin}${folder}/${receiver}`); } /** * Safely joins server URL and path template, preventing duplicate or missing slashes. */ export function joinServerAndPath(serverUrl = "", path = "") { const cleanServer = (serverUrl || "").replace(/\/+$/, ""); const cleanPath = path ? `/${path.replace(/^\/+/, "")}` : ""; return `${cleanServer}${cleanPath}`; } /** * Constructs the final fetch URL with resolved path parameters, serialized query string, * and optional authentication tokens. */ export function constructFetchURL({ serverUrl = "", path = "", pathParams = {}, queryParams = [], apiKeys = [], } = {}) { let resolvedPath = replacePathParams(path, pathParams); const queryString = buildQueryString(queryParams); if (queryString) { resolvedPath = `${resolvedPath}${resolvedPath.includes("?") ? "&" : "?"}${queryString}`; } // Append query-based authentication parameters if (Array.isArray(apiKeys)) { apiKeys .filter((k) => k && k.in === "query" && k.finalKeyValue) .forEach((k) => { resolvedPath = `${resolvedPath}${resolvedPath.includes("?") ? "&" : "?"}${encodeURIComponent(k.name)}=${encodeURIComponent(k.finalKeyValue)}`; }); } return joinServerAndPath(serverUrl, resolvedPath); }