oas
Version:
Comprehensive tooling for working with OpenAPI definitions
507 lines (502 loc) • 16.6 kB
JavaScript
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