mirror of
https://github.com/Redocly/redoc.git
synced 2025-01-31 18:14:07 +03:00
219 lines
6.4 KiB
TypeScript
219 lines
6.4 KiB
TypeScript
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types';
|
|
import { isOperationName, JsonPointer } from '../utils';
|
|
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
import { GroupModel, OperationModel } from './models';
|
|
import { OpenAPIParser } from './OpenAPIParser';
|
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
|
|
|
export type TagInfo = OpenAPITag & {
|
|
operations: ExtendedOpenAPIOperation[];
|
|
used?: boolean;
|
|
};
|
|
|
|
export type ExtendedOpenAPIOperation = {
|
|
_$ref: string;
|
|
httpVerb: string;
|
|
pathParameters: Array<Referenced<OpenAPIParameter>>;
|
|
} & OpenAPIOperation;
|
|
|
|
export type TagsInfoMap = Dict<TagInfo>;
|
|
|
|
export interface TagGroup {
|
|
name: string;
|
|
tags: string[];
|
|
}
|
|
|
|
export const GROUP_DEPTH = 0;
|
|
export type ContentItemModel = GroupModel | OperationModel;
|
|
|
|
export class MenuBuilder {
|
|
/**
|
|
* Builds page content structure based on tags
|
|
*/
|
|
static buildStructure(
|
|
parser: OpenAPIParser,
|
|
options: RedocNormalizedOptions,
|
|
): ContentItemModel[] {
|
|
const spec = parser.spec;
|
|
|
|
const items: ContentItemModel[] = [];
|
|
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
|
|
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || ''));
|
|
if (spec['x-tagGroups']) {
|
|
items.push(
|
|
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
|
|
);
|
|
} else {
|
|
items.push(...MenuBuilder.getTagsItems(parser, tagsMap, undefined, undefined, options));
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/**
|
|
* extracts items from markdown description
|
|
* @param description - markdown source
|
|
*/
|
|
static addMarkdownItems(description: string): ContentItemModel[] {
|
|
const renderer = new MarkdownRenderer();
|
|
const headings = renderer.extractHeadings(description || '');
|
|
|
|
const mapHeadingsDeep = (parent, items, depth = 1) =>
|
|
items.map(heading => {
|
|
const group = new GroupModel('section', heading, parent);
|
|
group.depth = depth;
|
|
if (heading.items) {
|
|
group.items = mapHeadingsDeep(group, group.items, depth + 1);
|
|
}
|
|
return group;
|
|
});
|
|
|
|
return mapHeadingsDeep(undefined, headings);
|
|
}
|
|
|
|
/**
|
|
* Returns array of OperationsGroup items for the tag groups (x-tagGroups vendor extenstion)
|
|
* @param tags value of `x-tagGroups` vendor extension
|
|
*/
|
|
static getTagGroupsItems(
|
|
parser: OpenAPIParser,
|
|
parent: GroupModel | undefined,
|
|
groups: TagGroup[],
|
|
tags: TagsInfoMap,
|
|
options: RedocNormalizedOptions,
|
|
): GroupModel[] {
|
|
const res: GroupModel[] = [];
|
|
for (const group of groups) {
|
|
const item = new GroupModel('group', group, parent);
|
|
item.depth = GROUP_DEPTH;
|
|
item.items = MenuBuilder.getTagsItems(parser, tags, item, group, options);
|
|
res.push(item);
|
|
}
|
|
// TODO checkAllTagsUsedInGroups
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Returns array of OperationsGroup items for the tags of the group or for all tags
|
|
* @param tagsMap tags info returned from `getTagsWithOperations`
|
|
* @param parent parent item
|
|
* @param group group which this tag belongs to. if not provided gets all tags
|
|
*/
|
|
static getTagsItems(
|
|
parser: OpenAPIParser,
|
|
tagsMap: TagsInfoMap,
|
|
parent: GroupModel | undefined,
|
|
group: TagGroup | undefined,
|
|
options: RedocNormalizedOptions,
|
|
): ContentItemModel[] {
|
|
let tagNames;
|
|
|
|
if (group === undefined) {
|
|
tagNames = Object.keys(tagsMap); // all tags
|
|
} else {
|
|
tagNames = group.tags;
|
|
}
|
|
|
|
const tags = tagNames.map(tagName => {
|
|
if (!tagsMap[tagName]) {
|
|
console.warn(`Non-existing tag "${tagName}" is added to the group "${group!.name}"`);
|
|
return null;
|
|
}
|
|
tagsMap[tagName].used = true;
|
|
return tagsMap[tagName];
|
|
});
|
|
|
|
const res: Array<GroupModel | OperationModel> = [];
|
|
for (const tag of tags) {
|
|
if (!tag) {
|
|
continue;
|
|
}
|
|
const item = new GroupModel('tag', tag, parent);
|
|
item.depth = GROUP_DEPTH + 1;
|
|
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
|
|
|
|
// don't put empty tag into content, instead put its operations
|
|
if (tag.name === '') {
|
|
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options);
|
|
res.push(...items);
|
|
continue;
|
|
}
|
|
|
|
res.push(item);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Returns array of Operation items for the tag
|
|
* @param parent parent OperationsGroup
|
|
* @param tag tag info returned from `getTagsWithOperations`
|
|
* @param depth items depth
|
|
*/
|
|
static getOperationsItems(
|
|
parser: OpenAPIParser,
|
|
parent: GroupModel | undefined,
|
|
tag: TagInfo,
|
|
depth: number,
|
|
options: RedocNormalizedOptions,
|
|
): OperationModel[] {
|
|
if (tag.operations.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const res: OperationModel[] = [];
|
|
for (const operationInfo of tag.operations) {
|
|
const operation = new OperationModel(parser, operationInfo, parent, options);
|
|
operation.depth = depth;
|
|
res.push(operation);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* collects tags and maps each tag to list of operations belonging to this tag
|
|
*/
|
|
static getTagsWithOperations(spec: OpenAPISpec): TagsInfoMap {
|
|
const tags: TagsInfoMap = {};
|
|
for (const tag of spec.tags || []) {
|
|
tags[tag.name] = { ...tag, operations: [] };
|
|
}
|
|
|
|
const paths = spec.paths;
|
|
for (const pathName of Object.keys(paths)) {
|
|
const path = paths[pathName];
|
|
const operations = Object.keys(path).filter(isOperationName);
|
|
for (const operationName of operations) {
|
|
const operationInfo = path[operationName];
|
|
let operationTags = operationInfo.tags;
|
|
|
|
if (!operationTags || !operationTags.length) {
|
|
// empty tag
|
|
operationTags = [''];
|
|
}
|
|
const operationPointer = JsonPointer.compile(['paths', pathName, operationName]);
|
|
for (const tagName of operationTags) {
|
|
let tag = tags[tagName];
|
|
if (tag === undefined) {
|
|
tag = {
|
|
name: tagName,
|
|
operations: [],
|
|
};
|
|
tags[tagName] = tag;
|
|
}
|
|
if (tag['x-traitTag']) {
|
|
continue;
|
|
}
|
|
tag.operations.push({
|
|
...operationInfo,
|
|
_$ref: operationPointer,
|
|
httpVerb: operationName,
|
|
pathParameters: path.parameters || [],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return tags;
|
|
}
|
|
}
|