UNPKG

@redocly/openapi-core

Version:

See https://github.com/Redocly/openapi-cli

371 lines (334 loc) 10.7 kB
import isEqual = require('lodash.isequal'); import { BaseResolver, resolveDocument, Document, ResolvedRefMap, makeRefId } from './resolve'; import { Oas3Rule, normalizeVisitors, Oas3Visitor, Oas2Visitor } from './visitors'; import { Oas3Types } from './types/oas3'; import { Oas2Types } from './types/oas2'; import { Oas3_1Types } from './types/oas3_1'; import { NormalizedNodeType, normalizeTypes, NodeType } from './types'; import { WalkContext, walkDocument, UserContext, ResolveResult } from './walk'; import { detectOpenAPI, openAPIMajor, OasMajorVersion } from './oas-types'; import { isRef, Location, refBaseName } from './ref-utils'; import type { Config, LintConfig } from './config/config'; import { initRules } from './config/rules'; import { reportUnresolvedRef } from './rules/no-unresolved-refs'; import { isPlainObject } from './utils'; import { OasRef } from './typings/openapi'; import { isRedoclyRegistryURL } from './redocly'; import { RemoveUnusedComponents as RemoveUnusedComponentsOas2 } from './rules/oas2/remove-unused-components'; import { RemoveUnusedComponents as RemoveUnusedComponentsOas3 } from './rules/oas3/remove-unused-components'; export type Oas3RuleSet = Record<string, Oas3Rule>; export enum OasVersion { Version2 = 'oas2', Version3_0 = 'oas3_0', Version3_1 = 'oas3_1', } export async function bundle(opts: { ref?: string; doc?: Document; externalRefResolver?: BaseResolver; config: Config; dereference?: boolean; base?: string; skipRedoclyRegistryRefs?: boolean; removeUnusedComponents?: boolean; }) { const { ref, doc, externalRefResolver = new BaseResolver(opts.config.resolve), base = null, } = opts; if (!(ref || doc)) { throw new Error('Document or reference is required.\n'); } const document = doc !== undefined ? doc : await externalRefResolver.resolveDocument(base, ref!, true); if (document instanceof Error) { throw document; } return bundleDocument({ document, ...opts, config: opts.config.lint, externalRefResolver, }); } type BundleContext = WalkContext; export async function bundleDocument(opts: { document: Document; config: LintConfig; customTypes?: Record<string, NodeType>; externalRefResolver: BaseResolver; dereference?: boolean; skipRedoclyRegistryRefs?: boolean; removeUnusedComponents?: boolean; }) { const { document, config, customTypes, externalRefResolver, dereference = false, skipRedoclyRegistryRefs = false, removeUnusedComponents = false, } = opts; const oasVersion = detectOpenAPI(document.parsed); const oasMajorVersion = openAPIMajor(oasVersion); const rules = config.getRulesForOasVersion(oasMajorVersion); const types = normalizeTypes( config.extendTypes( customTypes ?? oasMajorVersion === OasMajorVersion.Version3 ? oasVersion === OasVersion.Version3_1 ? Oas3_1Types : Oas3Types : Oas2Types, oasVersion, ), config, ); const preprocessors = initRules(rules as any, config, 'preprocessors', oasVersion); const decorators = initRules(rules as any, config, 'decorators', oasVersion); const ctx: BundleContext = { problems: [], oasVersion: oasVersion, refTypes: new Map<string, NormalizedNodeType>(), visitorsData: {}, }; if (removeUnusedComponents) { decorators.push({ severity: 'error', ruleId: 'remove-unused-components', visitor: oasMajorVersion === OasMajorVersion.Version2 ? RemoveUnusedComponentsOas2({}) : RemoveUnusedComponentsOas3({}) }) } const resolvedRefMap = await resolveDocument({ rootDocument: document, rootType: types.DefinitionRoot, externalRefResolver, }); const bundleVisitor = normalizeVisitors( [ ...preprocessors, { severity: 'error', ruleId: 'bundler', visitor: makeBundleVisitor(oasMajorVersion, dereference, skipRedoclyRegistryRefs, document, resolvedRefMap), }, ...decorators, ], types, ); walkDocument({ document, rootType: types.DefinitionRoot as NormalizedNodeType, normalizedVisitors: bundleVisitor, resolvedRefMap, ctx, }); return { bundle: document, problems: ctx.problems.map((problem) => config.addProblemToIgnore(problem)), fileDependencies: externalRefResolver.getFiles(), rootType: types.DefinitionRoot, refTypes: ctx.refTypes, visitorsData: ctx.visitorsData, }; } export function mapTypeToComponent(typeName: string, version: OasMajorVersion) { switch (version) { case OasMajorVersion.Version3: switch (typeName) { case 'Schema': return 'schemas'; case 'Parameter': return 'parameters'; case 'Response': return 'responses'; case 'Example': return 'examples'; case 'RequestBody': return 'requestBodies'; case 'Header': return 'headers'; case 'SecuritySchema': return 'securitySchemes'; case 'Link': return 'links'; case 'Callback': return 'callbacks'; default: return null; } case OasMajorVersion.Version2: switch (typeName) { case 'Schema': return 'definitions'; case 'Parameter': return 'parameters'; case 'Response': return 'responses'; default: return null; } } } // function oas3Move function makeBundleVisitor( version: OasMajorVersion, dereference: boolean, skipRedoclyRegistryRefs: boolean, rootDocument: Document, resolvedRefMap: ResolvedRefMap ) { let components: Record<string, Record<string, any>>; const visitor: Oas3Visitor | Oas2Visitor = { ref: { leave(node, ctx, resolved) { if (!resolved.location || resolved.node === undefined) { reportUnresolvedRef(resolved, ctx.report, ctx.location); return; } if ( resolved.location.source === rootDocument.source && resolved.location.source === ctx.location.source && ctx.type.name !== 'scalar' && !dereference ) { return; } // do not bundle registry URL before push, otherwise we can't record dependencies if (skipRedoclyRegistryRefs && isRedoclyRegistryURL(node.$ref)) { return; } const componentType = mapTypeToComponent(ctx.type.name, version); if (!componentType) { replaceRef(node, resolved, ctx); } else { if (dereference) { saveComponent(componentType, resolved, ctx); replaceRef(node, resolved, ctx); } else { node.$ref = saveComponent(componentType, resolved, ctx); resolveBundledComponent(node, resolved, ctx); } } }, }, DefinitionRoot: { enter(root: any) { if (version === OasMajorVersion.Version3) { components = root.components = root.components || {}; } else if (version === OasMajorVersion.Version2) { components = root; } }, }, }; if (version === OasMajorVersion.Version3) { visitor.DiscriminatorMapping = { leave(mapping: Record<string, string>, ctx: any) { for (const name of Object.keys(mapping)) { const $ref = mapping[name]; const resolved = ctx.resolve({ $ref }); if (!resolved.location || resolved.node === undefined) { reportUnresolvedRef(resolved, ctx.report, ctx.location.child(name)); return; } const componentType = mapTypeToComponent('Schema', version)!; if (dereference) { saveComponent(componentType, resolved, ctx); } else { mapping[name] = saveComponent(componentType, resolved, ctx); } } }, }; } function resolveBundledComponent(node: OasRef, resolved: ResolveResult<any>, ctx: UserContext) { const newRefId = makeRefId(ctx.location.source.absoluteRef, node.$ref) resolvedRefMap.set(newRefId, { document: rootDocument, isRemote: false, node: resolved.node, nodePointer: node.$ref, resolved: true, }); } function replaceRef(ref: OasRef, resolved: ResolveResult<any>, ctx: UserContext) { if (!isPlainObject(resolved.node)) { ctx.parent[ctx.key] = resolved.node; } else { // @ts-ignore delete ref.$ref; Object.assign(ref, resolved.node); } } function saveComponent( componentType: string, target: { node: any; location: Location }, ctx: UserContext, ) { components[componentType] = components[componentType] || {}; const name = getComponentName(target, componentType, ctx); components[componentType][name] = target.node; if (version === OasMajorVersion.Version3) { return `#/components/${componentType}/${name}`; } else { return `#/${componentType}/${name}`; } } function isEqualOrEqualRef( node: any, target: { node: any; location: Location }, ctx: UserContext, ) { if ( isRef(node) && ctx.resolve(node).location?.absolutePointer === target.location.absolutePointer ) { return true; } return isEqual(node, target.node); } function getComponentName( target: { node: any; location: Location }, componentType: string, ctx: UserContext, ) { const [fileRef, pointer] = [target.location.source.absoluteRef, target.location.pointer]; const componentsGroup = components[componentType]; let name = ''; const refParts = pointer.slice(2).split('/').filter(Boolean); // slice(2) removes "#/" while (refParts.length > 0) { name = refParts.pop() + (name ? `-${name}` : ''); if ( !componentsGroup || !componentsGroup[name] || isEqualOrEqualRef(componentsGroup[name], target, ctx) ) { return name; } } name = refBaseName(fileRef) + (name ? `_${name}` : ''); if (!componentsGroup[name] || isEqualOrEqualRef(componentsGroup[name], target, ctx)) { return name; } const prevName = name; let serialId = 2; while (componentsGroup[name] && !isEqualOrEqualRef(componentsGroup[name], target, ctx)) { name = `${prevName}-${serialId}`; serialId++; } if (!componentsGroup[name]) { ctx.report({ message: `Two schemas are referenced with the same name but different content. Renamed ${prevName} to ${name}.`, location: ctx.location, forceSeverity: 'warn', }); } return name; } return visitor; }