oas
Version:
Comprehensive tooling for working with OpenAPI definitions
1 lines • 11.8 kB
Source Map (JSON)
{"version":3,"sources":["/Users/erunion/code/readme/oas/packages/oas/dist/reducer/index.cjs","../../src/reducer/index.ts"],"names":[],"mappings":"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","file":"/Users/erunion/code/readme/oas/packages/oas/dist/reducer/index.cjs","sourcesContent":[null,"import type { ComponentsObject, HttpMethods, OASDocument, TagObject } from '../types.js';\n\nimport jsonPointer from 'jsonpointer';\n\nimport { query } from '../analyzer/util.js';\n\ninterface ReducerOptions {\n /** A key-value object of path + method combinations to reduce by. */\n paths?: Record<string, string[] | '*'>;\n /** An array of tags in the OpenAPI definition to reduce by. */\n tags?: string[];\n}\n\n/**\n * Query a JSON Schema object for any `$ref` pointers. Return any pointers that were found.\n *\n * @param schema JSON Schema object to look for any `$ref` pointers within it.\n */\nfunction getUsedRefs(schema: any) {\n return query([\"$..['$ref']\"], schema);\n}\n\n/**\n * Recursively process a `$ref` pointer and accumulate any other `$ref` pointers that it or its\n * children use.\n *\n * @param schema JSON Schema object to look for and accumulate any `$ref` pointers that it may have.\n * @param $refs Known set of `$ref` pointers.\n * @param $ref `$ref` pointer to fetch a schema from out of the supplied schema.\n */\nfunction accumulateUsedRefs(schema: Record<string, unknown>, $refs: Set<string>, $ref: any): void {\n let $refSchema;\n if (typeof $ref === 'string') $refSchema = jsonPointer.get(schema, $ref.substring(1));\n if ($refSchema === undefined) {\n // If the schema we have wasn't fully dereferenced or bundled for whatever reason and this\n // `$ref` that we have doesn't exist here we shouldn't try to search for more `$ref` pointers\n // in a schema that doesn't exist.\n return;\n }\n\n getUsedRefs($refSchema).forEach(({ value: currRef }) => {\n // Because it's possible to have a parameter named `$ref`, which our lookup would pick up as a\n // false positive, we want to exclude that from `$ref` matching as it's not really a reference.\n if (typeof currRef !== 'string') {\n return;\n }\n\n // If we've already processed this $ref don't send us into an infinite loop.\n if ($refs.has(currRef)) {\n return;\n }\n\n $refs.add(currRef);\n accumulateUsedRefs(schema, $refs, currRef);\n });\n}\n\n/**\n * With an array of tags or object of paths+method combinations, reduce an OpenAPI definition to a\n * new definition that just contains those tags or path + methods.\n *\n * @example <caption>Reduce by an array of tags only.</caption>\n * reducer(apiDefinition, { tags: ['pet'] })\n *\n * @example <caption>Reduce by a specific path and methods.</caption>\n * reducer(apiDefinition, { paths: { '/pet': ['get', 'post'] } })\n *\n * @example <caption>Reduce by a specific path and all methods it has.</caption>\n * reducer(apiDefinition, { paths: { '/pet': '*' } })\n *\n * @param definition A valid OpenAPI 3.x definition\n */\nexport default function reducer(definition: OASDocument, opts: ReducerOptions = {}): OASDocument {\n // Convert tags and paths to lowercase since casing should not matter.\n const reduceTags = 'tags' in opts ? opts.tags.map(tag => tag.toLowerCase()) : [];\n const reducePaths =\n 'paths' in opts\n ? Object.entries(opts.paths).reduce((acc: Record<string, string[] | string>, [key, value]) => {\n const newKey = key.toLowerCase();\n const newValue = Array.isArray(value) ? value.map(v => v.toLowerCase()) : value.toLowerCase();\n acc[newKey] = newValue;\n return acc;\n }, {})\n : {};\n\n const $refs: Set<string> = new Set();\n const usedTags: Set<string> = new Set();\n\n if (!definition.openapi) {\n throw new Error('Sorry, only OpenAPI definitions are supported.');\n }\n\n // Stringify and parse so we get a full non-reference clone of the API definition to work with.\n // eslint-disable-next-line try-catch-failsafe/json-parse\n const reduced = JSON.parse(JSON.stringify(definition)) as OASDocument;\n\n // Retain any root-level security definitions.\n if ('security' in reduced) {\n Object.values(reduced.security).forEach(sec => {\n Object.keys(sec).forEach(scheme => {\n $refs.add(`#/components/securitySchemes/${scheme}`);\n });\n });\n }\n\n if ('paths' in reduced) {\n Object.keys(reduced.paths).forEach(path => {\n const pathLC = path.toLowerCase();\n\n if (Object.keys(reducePaths).length) {\n if (!(pathLC in reducePaths)) {\n delete reduced.paths[path];\n return;\n }\n }\n\n Object.keys(reduced.paths[path]).forEach((method: HttpMethods | 'parameters') => {\n // If this method is `parameters` we should always retain it.\n if (method !== 'parameters') {\n if (Object.keys(reducePaths).length) {\n if (\n reducePaths[pathLC] !== '*' &&\n Array.isArray(reducePaths[pathLC]) &&\n !reducePaths[pathLC].includes(method)\n ) {\n delete reduced.paths[path][method];\n return;\n }\n }\n }\n\n const operation = reduced.paths[path][method];\n\n // If we're reducing by tags and this operation doesn't live in one of those, remove it.\n if (reduceTags.length) {\n if (!('tags' in operation)) {\n delete reduced.paths[path][method];\n return;\n } else if (!operation.tags.filter(tag => reduceTags.includes(tag.toLowerCase())).length) {\n delete reduced.paths[path][method];\n return;\n }\n }\n\n // Accumulate a list of used tags so we can filter out any ones that we don't need later.\n if ('tags' in operation) {\n operation.tags.forEach((tag: string) => {\n usedTags.add(tag);\n });\n }\n\n // Accumulate a list of $ref pointers that are used within this operation.\n getUsedRefs(operation).forEach(({ value: ref }) => {\n $refs.add(ref);\n });\n\n // Accumulate any used security schemas that we need to retain.\n if ('security' in operation) {\n Object.values(operation.security).forEach(sec => {\n Object.keys(sec).forEach(scheme => {\n $refs.add(`#/components/securitySchemes/${scheme}`);\n });\n });\n }\n });\n\n // If this path no longer has any methods, delete it.\n if (!Object.keys(reduced.paths[path]).length) {\n delete reduced.paths[path];\n }\n });\n\n // If we don't have any more paths after cleanup, throw an error because an OpenAPI file must\n // have at least one path.\n if (!Object.keys(reduced.paths).length) {\n throw new Error('All paths in the API definition were removed. Did you supply the right path name to reduce by?');\n }\n }\n\n // Recursively accumulate any components that are in use.\n $refs.forEach($ref => accumulateUsedRefs(reduced, $refs, $ref));\n\n // Remove any unused components.\n if ('components' in reduced) {\n Object.keys(reduced.components).forEach((componentType: keyof ComponentsObject) => {\n Object.keys(reduced.components[componentType]).forEach(component => {\n // If our `$ref` either is a full, or deep match, then we should preserve it.\n const refIsUsed =\n $refs.has(`#/components/${componentType}/${component}`) ||\n Array.from($refs).some(ref => {\n // Because you can have a `$ref` like `#/components/examples/event-min/value`, which\n // would be accumulated via our `$refs` query, we want to make sure we account for them.\n // If we don't look for these then we'll end up removing them from the overall reduced\n // definition, resulting in data loss and schema corruption.\n return ref.startsWith(`#/components/${componentType}/${component}/`);\n });\n\n if (!refIsUsed) {\n delete reduced.components[componentType][component];\n }\n });\n\n // If this component group is now empty, delete it.\n if (!Object.keys(reduced.components[componentType]).length) {\n delete reduced.components[componentType];\n }\n });\n\n // If this path no longer has any components, delete it.\n if (!Object.keys(reduced.components).length) {\n delete reduced.components;\n }\n }\n\n // Remove any unused tags.\n if ('tags' in reduced) {\n reduced.tags.forEach((tag: TagObject, k: number) => {\n if (!usedTags.has(tag.name)) {\n delete reduced.tags[k];\n }\n });\n\n // Remove any now empty items from the tags array.\n reduced.tags = reduced.tags.filter(Boolean);\n\n if (!reduced.tags.length) {\n delete reduced.tags;\n }\n }\n\n return reduced;\n}\n"]}