UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

107 lines (101 loc) • 3.98 kB
/** * Looks up the OpenAPI operation an Arazzo step calls, without dereferencing the OpenAPI document. */ const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace", "query"]; /** * Indexes an OpenAPI document's operations by operationId and by "<method> <path>". * * @param {object} openapi Raw OpenAPI 2.0, 3.x object * @returns {{ byId: Map<string, object>, byPath: Map<string, object> }} values are * `{ operationId, method, path, summary, security }`, method lower-case; `security` is the * operation's requirement, else the document's */ export function indexOperations(openapi) { const byId = new Map(); const byPath = new Map(); for (const [path, item] of Object.entries(openapi?.paths || {})) { if (!item || typeof item !== "object") continue; for (const method of METHODS) { const op = item[method]; if (!op || typeof op !== "object") continue; const entry = { operationId: op.operationId || "", method, path, summary: op.summary || "", }; const security = op.security ?? openapi.security; if (security !== undefined) entry.security = security; if (op.operationId) byId.set(op.operationId, entry); byPath.set(`${method} ${path}`, entry); } } return { byId, byPath }; } /** * The same index from a spec <rapi-doc> has already processed (its `resolvedSpec`), whose * operations are listed under `tags[].paths[]`. Webhooks are skipped. An operation without an * operationId in the document gets one generated by <rapi-doc>, which no Arazzo step can name, * so it only matters for lookup by method and path. * * @param {{ tags?: { paths?: object[] }[] }} resolvedSpec * @returns {{ byId: Map<string, object>, byPath: Map<string, object> }} */ export function indexResolvedSpec(resolvedSpec) { const byId = new Map(); const byPath = new Map(); for (const tag of resolvedSpec?.tags || []) { for (const p of tag?.paths || []) { if (!p?.method || !p.path || p.isWebhook) continue; const method = p.method.toLowerCase(); const entry = { operationId: p.operationId || "", method, path: p.path, summary: p.summary || "", }; if (p.security !== undefined) entry.security = p.security; // An operation with several tags is listed once per tag; the first entry is kept const key = `${method} ${p.path}`; if (byPath.has(key)) continue; if (p.operationId) byId.set(p.operationId, entry); byPath.set(key, entry); } } return { byId, byPath }; } function unescapePointer(segment) { return decodeURIComponent(segment).replace(/~1/g, "/").replace(/~0/g, "~"); } /** * Resolves a step's operationId or operationPath. * * @param {object} step Arazzo step * @param {Map<string, {byId: Map, byPath: Map}>} sources indexes by source description name * @returns {{ source: string, operationId: string, method: string, path: string, summary: string } | null} */ export function resolveOperation(step, sources) { if (!sources?.size) return null; if (step.operationId) { let source = ""; let id = step.operationId; const qualified = id.match(/^\$sourceDescriptions\.([A-Za-z0-9_-]+)\.(.+)$/); if (qualified) [, source, id] = qualified; for (const [name, index] of sources) { if (source && name !== source) continue; const op = index.byId.get(id); if (op) return { source: name, ...op }; } return null; } if (step.operationPath) { const m = step.operationPath.match(/^\{\$sourceDescriptions\.([A-Za-z0-9_-]+)\.url\}#(.*)$/); if (!m) return null; const [, source, pointer] = m; const parts = pointer.split("/").slice(1).map(unescapePointer); if (parts.length !== 3 || parts[0] !== "paths") return null; const op = sources.get(source)?.byPath.get(`${parts[2].toLowerCase()} ${parts[1]}`); return op ? { source, ...op } : null; } return null; }