@redocly/openapi-core
Version:
See https://github.com/Redocly/openapi-cli
371 lines (334 loc) • 10.7 kB
text/typescript
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;
}