UNPKG

oas

Version:

Comprehensive tooling for working with OpenAPI definitions

507 lines (502 loc) 16.6 kB
import { query, refizePointer } from "../chunk-QTPMJRIE.js"; import { collectRefsInSchema, encodePointer, supportedMethods, toPointer } from "../chunk-VSILRJVM.js"; import "../chunk-LZNTJ4ZR.js"; import { isOpenAPI31 } from "../chunk-XG4HGNCN.js"; // src/analyzer/dereference.ts import { dereference } from "@readme/openapi-parser"; function getDereferencingOptions(circularRefs) { return { resolve: { // We shouldn't be resolving external pointers at this point so just ignore them. external: false }, dereference: { // If circular `$refs` are ignored they'll remain in the schema as `$ref: String`, otherwise // `$ref` just won't exist. This, in tandem with `onCircular`, allows us to do easy and // accumulate a list of circular references. circular: "ignore", onCircular: (path) => { circularRefs.add(`#${path.split("#")[1]}`); } } }; } var dereferenceStates = /* @__PURE__ */ new WeakMap(); function getDereferenceState(definition) { let state = dereferenceStates.get(definition); if (!state) { state = { processing: false, complete: false, circularRefs: [], promises: [] }; dereferenceStates.set(definition, state); } return state; } async function dereferenceOas(definition, opts) { const state = getDereferenceState(definition); if (state.complete && state.result) { return state.result; } if (state.processing) { return new Promise((resolve, reject) => { state.promises.push({ resolve, reject }); }); } state.processing = true; const circularRefs = /* @__PURE__ */ new Set(); const dereferencingOptions = getDereferencingOptions(circularRefs); const { promises } = state; return dereference(definition, dereferencingOptions).then((dereferenced) => { const result = { api: dereferenced, circularRefs: [...circularRefs] }; state.result = result; state.circularRefs = result.circularRefs; state.processing = false; state.complete = true; if (opts?.cb) { opts.cb(); } return result; }).then((result) => { promises.forEach((deferred) => deferred.resolve(result)); state.promises = []; return result; }).catch((err) => { state.processing = false; promises.forEach((deferred) => deferred.reject(err)); state.promises = []; throw err; }); } function dereferenceOasShared(definition) { let clone = sharedClones.get(definition); if (!clone) { clone = structuredClone(definition); sharedClones.set(definition, clone); } return dereferenceOas(clone); } var sharedClones = /* @__PURE__ */ new WeakMap(); // src/analyzer/query-cache.ts var cache = /* @__PURE__ */ new WeakMap(); function queryCached(queries, definition) { let byQuery = cache.get(definition); if (!byQuery) { byQuery = /* @__PURE__ */ new Map(); cache.set(definition, byQuery); } const cacheKey = queries.join(" "); let results = byQuery.get(cacheKey); if (!results) { results = query(queries, definition); byQuery.set(cacheKey, results); } return results; } // src/analyzer/scope.ts import jsonPointer from "jsonpointer"; function resolveKey(keys, target) { return keys.find((key) => key === target) || keys.find((key) => key.toLowerCase() === target.toLowerCase()); } function accumulateReachableRefs(definition, seeds) { const reachable = /* @__PURE__ */ new Set(); const queue = [...seeds]; while (queue.length) { const ref = queue.shift(); if (reachable.has(ref)) { continue; } reachable.add(ref); let resolved; try { resolved = jsonPointer.get(definition, toPointer(ref)); } catch { continue; } if (resolved === void 0) { continue; } collectRefsInSchema(resolved).forEach((nestedRef) => { if (!reachable.has(nestedRef)) { queue.push(nestedRef); } }); } return reachable; } function buildAnchors(rootPointer, extraPointers, reachableRefs) { const anchors = [rootPointer, ...extraPointers]; reachableRefs.forEach((ref) => anchors.push(toPointer(ref))); return { anchors, anchorPrefixes: anchors.map((anchor) => `${anchor}/`) }; } function collectSecuritySchemeRefs(security) { if (!Array.isArray(security)) { return []; } const refs = []; security.forEach((requirement) => { if (requirement && typeof requirement === "object") { Object.keys(requirement).forEach((scheme) => { refs.push(`#/components/securitySchemes/${scheme}`); }); } }); return refs; } function computeOperationScope(definition, path, method) { const pathKey = resolveKey(Object.keys(definition.paths || {}), path); if (!pathKey) { throw new Error(`Path \`${path}\` not found.`); } const pathItem = definition.paths[pathKey] || {}; const methodKey = resolveKey(Object.keys(pathItem), method); if (!methodKey || !supportedMethods.includes(methodKey.toLowerCase())) { throw new Error(`Operation \`${method} ${path}\` not found.`); } const operation = pathItem[methodKey]; const rootPointer = `/paths/${encodePointer(pathKey)}/${methodKey}`; const extraPointers = []; const seeds = new Set(collectRefsInSchema(operation)); if (pathItem.parameters) { extraPointers.push(`/paths/${encodePointer(pathKey)}/parameters`); collectRefsInSchema(pathItem.parameters).forEach((ref) => seeds.add(ref)); } const security = "security" in operation ? operation.security : definition.security; collectSecuritySchemeRefs(security).forEach((ref) => seeds.add(ref)); const reachableRefs = accumulateReachableRefs(definition, seeds); return { rootPointer, extraPointers, reachableRefs, ...buildAnchors(rootPointer, extraPointers, reachableRefs) }; } function computeWebhookScope(definition, webhookName, method) { const webhooks2 = "webhooks" in definition ? definition.webhooks : {}; const webhookKey = resolveKey(Object.keys(webhooks2 || {}), webhookName); if (!webhookKey) { throw new Error(`Webhook \`${webhookName}\` not found.`); } const webhook = webhooks2[webhookKey] || {}; const methodKey = resolveKey(Object.keys(webhook), method); if (!methodKey || !supportedMethods.includes(methodKey.toLowerCase())) { throw new Error(`Webhook operation \`${method} ${webhookName}\` not found.`); } const operation = webhook[methodKey]; const rootPointer = `/webhooks/${encodePointer(webhookKey)}/${methodKey}`; const extraPointers = []; const seeds = new Set(collectRefsInSchema(operation)); if (webhook.parameters) { extraPointers.push(`/webhooks/${encodePointer(webhookKey)}/parameters`); collectRefsInSchema(webhook.parameters).forEach((ref) => seeds.add(ref)); } const security = "security" in operation ? operation.security : definition.security; collectSecuritySchemeRefs(security).forEach((ref) => seeds.add(ref)); const reachableRefs = accumulateReachableRefs(definition, seeds); return { rootPointer, extraPointers, reachableRefs, ...buildAnchors(rootPointer, extraPointers, reachableRefs) }; } function isPointerInScope(pointer, scope) { const { anchors, anchorPrefixes } = scope; for (let i = 0; i < anchors.length; i += 1) { if (pointer === anchors[i] || pointer.startsWith(anchorPrefixes[i])) { return true; } } return false; } function isAncestorOfScope(pointer, scope) { return scope.rootPointer === pointer || scope.rootPointer.startsWith(`${pointer}/`); } // src/analyzer/queries/openapi.ts function filterByScope(results, scope) { if (!scope) { return results; } return results.filter((res) => isPointerInScope(res.pointer, scope)); } function additionalProperties(definition, scope) { return filterByScope(queryCached(["$..additionalProperties"], definition), scope).map( (res) => refizePointer(res.pointer) ); } function callbacks(definition, scope) { return filterByScope( queryCached(["$.components.callbacks", "$.paths.*[?(@.callbacks)].callbacks"], definition), scope ).map((res) => refizePointer(res.pointer)); } function commonParameters(definition, scope) { return filterByScope(queryCached(["$..paths[*].parameters"], definition), scope).map( (res) => refizePointer(res.pointer) ); } function discriminators(definition, scope) { return filterByScope(queryCached(["$..discriminator"], definition), scope).map((res) => refizePointer(res.pointer)); } function links(definition, scope) { return filterByScope(queryCached(["$.components.links", "$.paths..responses.*.links"], definition), scope).map( (res) => refizePointer(res.pointer) ); } function mediaTypes(definition, scope) { const results = Array.from( new Set( filterByScope( queryCached( [ "$..paths..content", "$.components.requestBodies..content", "$.components.responses..content", "$.webhooks..content" ], definition ), scope ).flatMap((res) => { return Object.keys(res.value); }) ) ); results.sort(); return results; } function parameterSerialization(definition, scope) { return filterByScope(queryCached(["$..parameters[*].style^"], definition), scope).map( (res) => refizePointer(res.pointer) ); } function polymorphism(definition, scope) { const results = Array.from( new Set( filterByScope(queryCached(["$..allOf^", "$..anyOf^", "$..oneOf^"], definition), scope).map( (res) => refizePointer(res.pointer) ) ) ); results.sort(); return results; } function references(definition, scope) { return filterByScope(queryCached(["$..$ref^"], definition), scope).map((res) => refizePointer(res.pointer)); } function securityTypes(definition, scope) { return Array.from( new Set( filterByScope(queryCached(["$.components.securitySchemes..type"], definition), scope).map( (res) => res.value ) ) ); } function serverVariables(definition) { return queryCached(["$.servers..variables^"], definition).map((res) => refizePointer(res.pointer)); } function totalOperations(definition) { return queryCached(["$..paths[*]"], definition).flatMap((res) => Object.keys(res.value)).length; } function webhooks(definition, scope) { const results = queryCached(["$.webhooks[*]"], definition); if (!scope) { return results.map((res) => refizePointer(res.pointer)); } return results.filter((res) => isPointerInScope(res.pointer, scope) || isAncestorOfScope(res.pointer, scope)).map((res) => refizePointer(res.pointer)); } function xmlRequests(definition, scope) { return filterByScope( queryCached( [ "$..requestBody..['application/xml']", "$..requestBody..['application/xml-external-parsed-entity']", "$..requestBody..['application/xml-dtd']", "$..requestBody..['text/xml']", "$..requestBody..['text/xml-external-parsed-entity']", "$..requestBody.content[?(@property.match(/\\+xml$/i))]" ], definition ), scope ).map((res) => refizePointer(res.pointer)); } function xmlResponses(definition, scope) { return filterByScope( queryCached( [ "$..responses..['application/xml']", "$..responses..['application/xml-external-parsed-entity']", "$..responses..['application/xml-dtd']", "$..responses..['text/xml']", "$..responses..['text/xml-external-parsed-entity']", "$..responses[*].content[?(@property.match(/\\+xml$/i))]" ], definition ), scope ).map((res) => refizePointer(res.pointer)); } function xmlSchemas(definition, scope) { return filterByScope( queryCached(["$.components.schemas..xml^", "$..parameters..xml^", "$..requestBody..xml^"], definition), scope ).map((res) => refizePointer(res.pointer)); } // src/analyzer/index.ts async function buildAnalysis(definition, query2, scope) { const analysis = {}; if (!query2 || query2.includes("mediaTypes")) { analysis.mediaTypes = { name: "Media Type", found: mediaTypes(definition, scope) }; } if (!query2 || query2.includes("operationTotal")) { analysis.operationTotal = { name: "Operation", // A scoped analysis is, by definition, looking at exactly one operation. found: scope ? 1 : totalOperations(definition) }; } if (!query2 || query2.includes("securityTypes")) { analysis.securityTypes = { name: "Security Type", found: securityTypes(definition, scope) }; } if (!query2 || query2.includes("additionalProperties")) { const additionalProperties2 = additionalProperties(definition, scope); analysis.additionalProperties = { present: !!additionalProperties2.length, locations: additionalProperties2 }; } if (!query2 || query2.includes("callbacks")) { const callbacks2 = callbacks(definition, scope); analysis.callbacks = { present: !!callbacks2.length, locations: callbacks2 }; } if (!query2 || query2.includes("circularRefs")) { const { circularRefs: dereferencedCircularRefs } = await dereferenceOasShared(definition); const circularRefs = (scope ? dereferencedCircularRefs.filter((ref) => isPointerInScope(toPointer(ref), scope)) : [...dereferencedCircularRefs]).toSorted(); analysis.circularRefs = { present: !!circularRefs.length, locations: circularRefs }; } if (!query2 || query2.includes("commonParameters")) { const commonParameters2 = commonParameters(definition, scope); analysis.commonParameters = { present: !!commonParameters2.length, locations: commonParameters2 }; } if (!query2 || query2.includes("discriminators")) { const discriminators2 = discriminators(definition, scope); analysis.discriminators = { present: !!discriminators2.length, locations: discriminators2 }; } if (!query2 || query2.includes("links")) { const links2 = links(definition, scope); analysis.links = { present: !!links2.length, locations: links2 }; } if (!query2 || query2.includes("style")) { const parameterSerialization2 = parameterSerialization(definition, scope); analysis.style = { present: !!parameterSerialization2.length, locations: parameterSerialization2 }; } if (!query2 || query2.includes("polymorphism")) { const polymorphism2 = polymorphism(definition, scope); analysis.polymorphism = { present: !!polymorphism2.length, locations: polymorphism2 }; } if (!query2 || query2.includes("references")) { const references2 = references(definition, scope); analysis.references = { present: !!references2.length, locations: references2 }; } if (!query2 || query2.includes("serverVariables")) { const serverVariables2 = serverVariables(definition); analysis.serverVariables = { present: !!serverVariables2.length, locations: serverVariables2 }; } if (!query2 || query2.includes("xmlRequests")) { const xmlRequests2 = xmlRequests(definition, scope); analysis.xmlRequests = { present: !!xmlRequests2.length, locations: xmlRequests2 }; } if (!query2 || query2.includes("xmlResponses")) { const xmlResponses2 = xmlResponses(definition, scope); analysis.xmlResponses = { present: !!xmlResponses2.length, locations: xmlResponses2 }; } if (!query2 || query2.includes("xmlSchemas")) { const xmlSchemas2 = xmlSchemas(definition, scope); analysis.xmlSchemas = { present: !!xmlSchemas2.length, locations: xmlSchemas2 }; } if (!query2 || query2.includes("xmlSchemas")) { const webhooks2 = webhooks(definition, scope); analysis.webhooks = { present: !!webhooks2.length, locations: webhooks2 }; } return analysis; } async function analyzer(definition, query2) { return buildAnalysis(definition, query2); } async function analyzeOperation(definition, { path, method, query: query2 }) { const scope = computeOperationScope(definition, path, method); return buildAnalysis(definition, query2, scope); } async function analyzeWebhookOperation(definition, { webhookName, method, query: query2 }) { if (!isOpenAPI31(definition)) { throw new TypeError("The supplied definition is not a OpenAPI 3.1 definition."); } const scope = computeWebhookScope(definition, webhookName, method); return buildAnalysis(definition, query2, scope); } export { analyzeOperation, analyzeWebhookOperation, analyzer }; //# sourceMappingURL=index.js.map