UNPKG

oas

Version:

Comprehensive tooling for working with OpenAPI definitions

584 lines (583 loc) 21.9 kB
import { query } from "../chunk-CKC36IL7.js"; import { decodePointer, supportedMethods } from "../chunk-65ZD6K3I.js"; import "../chunk-S27IGTVG.js"; import { isOpenAPI31, isRef } from "../chunk-PSNTODZL.js"; // src/reducer/index.ts import jsonPointer from "jsonpointer"; var OpenAPIReducer = class _OpenAPIReducer { definition; /** * A collection of `$ref` pointers that are used within our reduced API definition. This is used * to ensure that all referenced schemas are retained in our resulting API definition. Not * retaining them would result in an invalid OpenAPI definition. */ $refs = /* @__PURE__ */ new Set(); /** * A collection of OpenAPI tags that are used within our reduced API definition. */ usedTags = /* @__PURE__ */ new Set(); /** * A collection of OpenAPI paths and operations that are cross-referenced from any other paths * and operations that we are reducing. This collection is used in order to ensure that those * schemas are retained with our resulting API definition. Not retaining them would result in an * invalid OpenAPI definition. */ retainPathMethods = /* @__PURE__ */ new Set(); /** * A collection of OpenAPI webhook names and methods that are cross-referenced from any other * schemas. This collection, like `retainPathMethods`, is used in order to ensure that those * schemas are retained with our resulting API definition. Not retaining them would result in an * invalid OpenAPI definition. */ retainWebhookMethods = /* @__PURE__ */ new Set(); /** * An array of OpenAPI tags to reduce down to. */ tagsToReduceBy = []; /** * A collection of OpenAPI paths and operations to reduce down to. */ pathsToReduceBy = {}; /** * A collection of OpenAPI webhooks to reduce down to. */ webhooksToReduceBy = {}; hasTagsToReduceBy = false; hasPathsToReduceBy = false; hasWebhooksToReduceBy = false; constructor(definition) { this.definition = structuredClone(definition); } /** * Initialize a new instance of the `OpenAPIReducer`. The reducer allows you to reduce an OpenAPI * definition down to only the information necessary to fulfill a specific set of tags, paths, * operations, and webhooks. * * OpenAPI reduction can be helpful not only to isolate and troubleshoot issues with large API * definitions, but also to compress a large API definition down to a manageable size containing * a specific set of items. * * All OpenAPI definitions reduced will still be fully functional and valid OpenAPI definitions. * * @param definition An OpenAPI definition to reduce. */ static init(definition) { return new _OpenAPIReducer(definition); } /** * Mark an OpenAPI tag to be included in our reduced API definition. Tag casing does not matter. * * @param tag The tag to mark for reduction. */ byTag(tag) { this.tagsToReduceBy.push(tag.toLowerCase()); return this; } /** * Mark an entire OpenAPI path, and all methods that it contains, to be included in your reduced * API definition. Path casing does not matter. * * @param path The path to mark for reduction. */ byPath(path) { this.pathsToReduceBy[path.toLowerCase()] = "*"; return this; } /** * Mark a single OpenAPI operation to be included in your reduced API definition. If the path * that this operation is a part of utilizes common parameters, those will be automatically * included. Path and method casing does not matter. * * Note that if you previously called `.byPath()` to reduce an entire path down, calling * `.byOperation()` will override that to just reduce this specific method (or this plus * subsequent calls to `.byOperation()`). * * @param path The path that the operation is a part of. * @param method The HTTP method of the operation to mark for reduction. * */ byOperation(path, method) { const pathLC = path.toLowerCase(); const methodLC = method.toLowerCase(); if (this.pathsToReduceBy[pathLC] && Array.isArray(this.pathsToReduceBy[pathLC])) { this.pathsToReduceBy[pathLC].push(methodLC); } else { this.pathsToReduceBy[pathLC] = [methodLC]; } return this; } byWebhook(webhookName, method) { const nameLC = webhookName.toLowerCase(); if (!method) { this.webhooksToReduceBy[nameLC] = "*"; return this; } const methodLC = method.toLowerCase(); if (this.webhooksToReduceBy[nameLC] && Array.isArray(this.webhooksToReduceBy[nameLC])) { this.webhooksToReduceBy[nameLC].push(methodLC); } else { this.webhooksToReduceBy[nameLC] = [methodLC]; } return this; } /** * Reduce the current OpenAPI definition down to the configured filters. * */ reduce() { if (!this.definition.openapi) { throw new Error("Sorry, only OpenAPI definitions are supported."); } this.hasPathsToReduceBy = Boolean(Object.keys(this.pathsToReduceBy).length); this.hasWebhooksToReduceBy = Boolean(Object.keys(this.webhooksToReduceBy).length); this.hasTagsToReduceBy = Boolean(this.tagsToReduceBy.length); if ("security" in this.definition) { Object.values(this.definition.security || {}).forEach((sec) => { Object.keys(sec).forEach((scheme) => { this.$refs.add(`#/components/securitySchemes/${scheme}`); }); }); } this.walkPaths(); this.walkWebhooks(); this.$refs.forEach(($ref) => { this.accumulateUsedRefs(this.definition, this.$refs, $ref); }); this.$refs.forEach((ref) => { const usedPathRef = this.parsePathRef(ref); if (usedPathRef) { this.retainPathMethods.add(`${usedPathRef.path.toLowerCase()}|${usedPathRef.method.toLowerCase()}`); } const usedWebhookRef = this.parseWebhookRef(ref); if (usedWebhookRef) { this.retainWebhookMethods.add(`${usedWebhookRef.name.toLowerCase()}|${usedWebhookRef.method.toLowerCase()}`); } }); this.reducePaths(); this.reduceWebhooks(); const hasPaths = Boolean(this.definition.paths && Object.keys(this.definition.paths).length); const hasWebhooks = Boolean( "webhooks" in this.definition && this.definition.webhooks && Object.keys(this.definition.webhooks).length ); if (!hasPaths && !hasWebhooks) { throw new Error( "All paths and webhooks in the API definition were removed. Did you supply the right path, operation, or webhook to reduce by?" ); } if ("components" in this.definition) { Object.keys(this.definition.components || {}).forEach((componentType) => { Object.keys(this.definition.components?.[componentType] || {}).forEach((component) => { const refIsUsed = this.$refs.has(`#/components/${componentType}/${component}`) || Array.from(this.$refs).some((ref) => { return ref.startsWith(`#/components/${componentType}/${component}/`); }); if (!refIsUsed) { delete this.definition.components?.[componentType]?.[component]; } }); if (!Object.keys(this.definition.components?.[componentType] || {}).length) { delete this.definition.components?.[componentType]; } }); if (!Object.keys(this.definition.components || {}).length) { delete this.definition.components; } } if ("tags" in this.definition) { this.definition.tags = (this.definition.tags ?? []).filter((tag) => { return Boolean(tag) && this.usedTags.has(tag.name); }); if (!this.definition.tags?.length) { delete this.definition.tags; } } return this.definition; } /** * Recursively process a `$ref` pointer and accumulate any other `$ref` pointers that it or its * children use. This handles circular references by skipping `$ref` pointers we have already seen. * Additionally when a `$ref` points to `#/paths` we record the used path + method so we can * retain cross-operation references within the reduced definition. * * @param schema JSON Schema object to look for and accumulate any `$ref` pointers that it may have. * @param $refs Known set of `$ref` pointers. * @param $ref `$ref` pointer to fetch a schema from out of the supplied schema. */ accumulateUsedRefs(schema, $refs, $ref) { const pathRef = this.parsePathRef($ref); if (pathRef) { this.retainPathMethods.add(`${pathRef.path.toLowerCase()}|${pathRef.method.toLowerCase()}`); } const webhookRef = this.parseWebhookRef($ref); if (webhookRef) { this.retainWebhookMethods.add(`${webhookRef.name.toLowerCase()}|${webhookRef.method.toLowerCase()}`); } let $refSchema; if (typeof $ref === "string") $refSchema = jsonPointer.get(schema, $ref.substring(1)); if ($refSchema === void 0) { return; } this.queryForRefPointers($refSchema).forEach(({ value: currRef }) => { const foundRef = this.toRefString(currRef); if (!foundRef) { return; } if ($refs.has(foundRef)) { return; } $refs.add(foundRef); this.accumulateUsedRefs(schema, $refs, foundRef); }); } /** * Query a JSON Schema object for any `$ref` pointers using JSONPath and return any pointers that * exist. * * @see {@link https://datatracker.ietf.org/doc/html/rfc9535} * @param schema JSON Schema object to look for any `$ref` pointers within it. */ queryForRefPointers(schema) { return query(["$..['$ref']"], schema); } /** * Normalize a value from a `jsonpath-plus` `$ref` query to a `$ref` pointer because JSONPath * queries may return the property value or the parent. * */ toRefString(value) { if (typeof value === "string") { return value; } else if (value && typeof value === "object" && "$ref" in value && typeof value.$ref === "string") { return value.$ref; } return null; } /** * If the given `$ref` points into a path (e.g. `#/paths/~1anything/post/...`), return the path * and method so the reducer can ultimately retain cross-operation references. * */ parsePathRef($ref) { if (typeof $ref !== "string" || !$ref.startsWith("#/paths/")) { return null; } const match = $ref.match(/^#\/paths\/([^/]+)\/([^/]+)(?:\/|$)/); if (match) { const pathSegment = match[1]; const method = match[2]; if (pathSegment && method) { return { path: decodePointer(pathSegment), method }; } } return null; } /** * If the given `$ref` points into webhooks (e.g. `#/webhooks/newBooking/post/...`), return the * webhook name and method so the reducer can retain cross-referenced webhook operations. * */ parseWebhookRef($ref) { if (typeof $ref !== "string" || !$ref.startsWith("#/webhooks/")) { return null; } const match = $ref.match(/^#\/webhooks\/([^/]+)\/([^/]+)(?:\/|$)/); if (match) { const webhookName = match[1]; const method = match[2]; if (webhookName && method) { return { name: decodePointer(webhookName), method }; } } return null; } /** * Walk through the `paths` in our OpenAPI definition and reduce down any that we know we do not * want to keep and accumulate any `$ref` pointers that we find that may be cross-referenced in * paths, webhooks, operations, and schemas that we _do_ want to keep. * */ walkPaths() { if (!("paths" in this.definition) || !this.definition.paths) { return; } Object.keys(this.definition.paths).forEach((path) => { const pathLC = path.toLowerCase(); if (this.hasWebhooksToReduceBy && !this.hasPathsToReduceBy) { delete this.definition.paths?.[path]; return; } if (this.hasPathsToReduceBy) { if (!(pathLC in this.pathsToReduceBy)) { delete this.definition.paths?.[path]; return; } } Object.keys(this.definition.paths?.[path] || {}).forEach((method) => { if (method === "parameters" || !supportedMethods.includes(method.toLowerCase())) { return; } if (this.hasPathsToReduceBy) { if (this.pathsToReduceBy[pathLC] !== "*" && Array.isArray(this.pathsToReduceBy[pathLC]) && !this.pathsToReduceBy[pathLC].includes(method.toLowerCase())) { return; } } const operation = this.definition.paths?.[path]?.[method]; if (!operation) { throw new Error(`Operation \`${method} ${path}\` not found`); } if (this.hasTagsToReduceBy) { if (!(operation.tags || []).filter((tag) => this.tagsToReduceBy.includes(tag.toLowerCase())).length) { return; } } (operation.tags || []).forEach((tag) => { this.usedTags.add(tag); }); const pathLevelParams = this.definition.paths?.[path]?.parameters; if (pathLevelParams) { this.queryForRefPointers(pathLevelParams).forEach(({ value: ref }) => { const refStr = this.toRefString(ref); if (!refStr) { return; } this.$refs.add(refStr); this.accumulateUsedRefs(this.definition, this.$refs, refStr); }); } this.queryForRefPointers(operation).forEach(({ value: ref }) => { const refStr = this.toRefString(ref); if (!refStr) { return; } this.$refs.add(refStr); const pathRef = this.parsePathRef(refStr); if (pathRef) { this.retainPathMethods.add(`${pathRef.path.toLowerCase()}|${pathRef.method.toLowerCase()}`); } this.accumulateUsedRefs(this.definition, this.$refs, refStr); }); Object.values(operation.security || {}).forEach((sec) => { Object.keys(sec).forEach((scheme) => { this.$refs.add(`#/components/securitySchemes/${scheme}`); }); }); }); }); } /** * Walk through the `webhooks` in our OpenAPI definition and reduce down any that we know we do * not want to keep and accumulate any `$ref` pointers that we find that may be cross-referenced * in paths, operations, and schemas that we _do_ want to keep. * */ walkWebhooks() { if (!isOpenAPI31(this.definition)) { return; } else if (!("webhooks" in this.definition) || !this.definition.webhooks) { return; } const definition = this.definition; Object.keys(definition.webhooks || {}).forEach((webhookName) => { const nameLC = webhookName.toLowerCase(); if (this.hasWebhooksToReduceBy && !(nameLC in this.webhooksToReduceBy)) { return; } const webhook = definition.webhooks?.[webhookName]; if (!webhook || typeof webhook !== "object") { return; } Object.keys(webhook).forEach((method) => { if (method === "parameters" || !supportedMethods.includes(method.toLowerCase())) { return; } if (this.hasWebhooksToReduceBy) { const methodFilter = this.webhooksToReduceBy[nameLC]; if (methodFilter !== "*" && Array.isArray(methodFilter) && !methodFilter.includes(method.toLowerCase())) { return; } } if (isRef(webhook)) { return; } const operation = webhook[method]; if (!operation) { return; } if (this.hasTagsToReduceBy) { if (!(operation.tags || []).filter((tag) => this.tagsToReduceBy.includes(tag.toLowerCase())).length) { return; } } (operation.tags || []).forEach((tag) => { this.usedTags.add(tag); }); if (webhook.parameters) { this.queryForRefPointers(webhook.parameters).forEach(({ value: ref }) => { const refStr = this.toRefString(ref); if (!refStr) { return; } this.$refs.add(refStr); this.accumulateUsedRefs(definition, this.$refs, refStr); }); } this.queryForRefPointers(operation).forEach(({ value: ref }) => { const refStr = this.toRefString(ref); if (!refStr) { return; } this.$refs.add(refStr); const pathRef = this.parsePathRef(refStr); if (pathRef) { this.retainPathMethods.add(`${pathRef.path.toLowerCase()}|${pathRef.method.toLowerCase()}`); } const webhookRef = this.parseWebhookRef(refStr); if (webhookRef) { this.retainWebhookMethods.add(`${webhookRef.name.toLowerCase()}|${webhookRef.method.toLowerCase()}`); } this.accumulateUsedRefs(definition, this.$refs, refStr); }); Object.values(operation.security || {}).forEach((sec) => { Object.keys(sec).forEach((scheme) => { this.$refs.add(`#/components/securitySchemes/${scheme}`); }); }); }); }); } /** * Prune back our `paths` object in the OpenAPI definition to only include paths that we want to * preserve. * */ reducePaths() { if (!("paths" in this.definition) || !this.definition.paths) { return; } Object.keys(this.definition.paths).forEach((path) => { const pathLC = path.toLowerCase(); if (this.hasPathsToReduceBy && !(pathLC in this.pathsToReduceBy)) { delete this.definition.paths?.[path]; return; } Object.keys(this.definition.paths?.[path] || {}).forEach((method) => { const methodLC = method.toLowerCase(); if (method === "parameters" || !supportedMethods.includes(methodLC)) { return; } const retainedByRef = this.retainPathMethods.has(`${pathLC}|${methodLC}`) || Array.from(this.$refs).some((ref) => { const pathRef = this.parsePathRef(ref); return pathRef?.path.toLowerCase() === pathLC && pathRef?.method.toLowerCase() === methodLC; }); if (methodLC !== "parameters") { if (this.hasPathsToReduceBy) { if (!retainedByRef && this.pathsToReduceBy[pathLC] !== "*" && Array.isArray(this.pathsToReduceBy[pathLC]) && !this.pathsToReduceBy[pathLC].includes(methodLC)) { delete this.definition.paths?.[path]?.[method]; return; } } } const operation = this.definition.paths?.[path]?.[method]; if (!operation) { throw new Error(`Operation \`${method} ${path}\` not found`); } if (this.hasTagsToReduceBy) { if (!(operation.tags || []).filter((tag) => this.tagsToReduceBy.includes(tag.toLowerCase())).length) { if (!retainedByRef) { delete this.definition.paths?.[path]?.[method]; } return; } } if ("tags" in operation) { operation.tags?.forEach((tag) => { this.usedTags.add(tag); }); } if ("security" in operation) { Object.values(operation.security || {}).forEach((sec) => { Object.keys(sec).forEach((scheme) => { this.$refs.add(`#/components/securitySchemes/${scheme}`); }); }); } }); if (!Object.keys(this.definition.paths?.[path] || {}).length) { delete this.definition.paths?.[path]; } }); if (!Object.keys(this.definition.paths || {}).length) { if (!(this.definition.webhooks && Object.keys(this.definition.webhooks).length)) { throw new Error( "All paths in the API definition were removed. Did you supply the right path name to reduce by?" ); } delete this.definition.paths; } } /** * Prune back our `webhooks` object in the OpenAPI definition to only include webhooks that we * want to preserve. * */ reduceWebhooks() { if (!isOpenAPI31(this.definition)) { return; } else if (!("webhooks" in this.definition) || !this.definition.webhooks) { return; } const definition = this.definition; Object.keys(definition.webhooks || {}).forEach((webhookName) => { const nameLC = webhookName.toLowerCase(); if (this.hasWebhooksToReduceBy && !(nameLC in this.webhooksToReduceBy)) { const retainedByRef = Array.from(this.retainWebhookMethods).some( (key) => key.startsWith(`${nameLC}|`) || key === `${nameLC}|` ); if (!retainedByRef) { delete definition.webhooks?.[webhookName]; return; } } const webhook = definition.webhooks?.[webhookName]; if (!webhook || typeof webhook !== "object") { return; } if (isRef(webhook)) { return; } Object.keys(webhook).forEach((method) => { const methodLC = method.toLowerCase(); if (method === "parameters" || !supportedMethods.includes(methodLC)) { return; } const retainedByRef = this.retainWebhookMethods.has(`${nameLC}|${methodLC}`); if (this.hasWebhooksToReduceBy && !retainedByRef) { const methodFilter = this.webhooksToReduceBy[nameLC]; if (methodFilter !== "*" && Array.isArray(methodFilter) && !methodFilter.includes(methodLC)) { if (!definition.webhooks?.[webhookName] || isRef(definition.webhooks?.[webhookName])) { return; } delete definition.webhooks?.[webhookName]?.[method]; } } }); if (!Object.keys(definition.webhooks?.[webhookName] || {}).length) { delete definition.webhooks?.[webhookName]; } }); if (definition.webhooks && !Object.keys(definition.webhooks).length) { delete definition.webhooks; } } }; export { OpenAPIReducer }; //# sourceMappingURL=index.js.map