redoc/src/services/MenuBuilder.ts

219 lines
6.4 KiB
TypeScript
Raw Normal View History

2017-10-12 00:01:37 +03:00
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types';
2018-01-22 21:30:53 +03:00
import { isOperationName, JsonPointer } from '../utils';
2017-10-12 00:01:37 +03:00
import { MarkdownRenderer } from './MarkdownRenderer';
2018-01-22 21:30:53 +03:00
import { GroupModel, OperationModel } from './models';
import { OpenAPIParser } from './OpenAPIParser';
2017-11-21 14:00:33 +03:00
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
2017-10-12 00:01:37 +03:00
export type TagInfo = OpenAPITag & {
operations: ExtendedOpenAPIOperation[];
used?: boolean;
};
export type ExtendedOpenAPIOperation = {
_$ref: string;
httpVerb: string;
2018-01-22 21:30:53 +03:00
pathParameters: Array<Referenced<OpenAPIParameter>>;
2017-10-12 00:01:37 +03:00
} & 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
*/
2017-11-21 14:00:33 +03:00
static buildStructure(
parser: OpenAPIParser,
options: RedocNormalizedOptions,
): ContentItemModel[] {
const spec = parser.spec;
2017-10-12 00:01:37 +03:00
const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || ''));
if (spec['x-tagGroups']) {
2017-11-21 14:00:33 +03:00
items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
);
2017-10-12 00:01:37 +03:00
} else {
2017-11-21 14:00:33 +03:00
items.push(...MenuBuilder.getTagsItems(parser, tagsMap, undefined, undefined, options));
2017-10-12 00:01:37 +03:00
}
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);
2017-10-12 00:01:37 +03:00
}
/**
* 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,
2017-11-21 14:00:33 +03:00
options: RedocNormalizedOptions,
2017-10-12 00:01:37 +03:00
): GroupModel[] {
2018-01-22 21:30:53 +03:00
const res: GroupModel[] = [];
for (const group of groups) {
const item = new GroupModel('group', group, parent);
2017-10-12 00:01:37 +03:00
item.depth = GROUP_DEPTH;
2017-11-21 14:00:33 +03:00
item.items = MenuBuilder.getTagsItems(parser, tags, item, group, options);
2017-10-12 00:01:37 +03:00
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,
2017-11-21 14:00:33 +03:00
parent: GroupModel | undefined,
group: TagGroup | undefined,
options: RedocNormalizedOptions,
2017-10-12 00:01:37 +03:00
): 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];
});
2018-01-22 21:30:53 +03:00
const res: Array<GroupModel | OperationModel> = [];
for (const tag of tags) {
if (!tag) {
continue;
}
const item = new GroupModel('tag', tag, parent);
2017-10-12 00:01:37 +03:00
item.depth = GROUP_DEPTH + 1;
2017-11-21 14:00:33 +03:00
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
2017-10-12 00:01:37 +03:00
// don't put empty tag into content, instead put its operations
if (tag.name === '') {
2018-01-22 21:30:53 +03:00
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options);
2017-10-12 00:01:37 +03:00
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,
2017-11-21 14:00:33 +03:00
options: RedocNormalizedOptions,
2017-10-12 00:01:37 +03:00
): OperationModel[] {
if (tag.operations.length === 0) {
return [];
}
2018-01-22 21:30:53 +03:00
const res: OperationModel[] = [];
for (const operationInfo of tag.operations) {
const operation = new OperationModel(parser, operationInfo, parent, options);
2017-10-12 00:01:37 +03:00
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 = {};
2018-01-22 21:30:53 +03:00
for (const tag of spec.tags || []) {
tags[tag.name] = { ...tag, operations: [] };
2017-10-12 00:01:37 +03:00
}
const paths = spec.paths;
2018-01-22 21:30:53 +03:00
for (const pathName of Object.keys(paths)) {
2017-10-12 00:01:37 +03:00
const path = paths[pathName];
const operations = Object.keys(path).filter(isOperationName);
2018-01-22 21:30:53 +03:00
for (const operationName of operations) {
2017-10-12 00:01:37 +03:00
const operationInfo = path[operationName];
let operationTags = operationInfo.tags;
if (!operationTags || !operationTags.length) {
// empty tag
operationTags = [''];
}
const operationPointer = JsonPointer.compile(['paths', pathName, operationName]);
2018-01-22 21:30:53 +03:00
for (const tagName of operationTags) {
2017-10-12 00:01:37 +03:00
let tag = tags[tagName];
if (tag === undefined) {
tag = {
name: tagName,
operations: [],
};
tags[tagName] = tag;
}
2018-01-22 21:30:53 +03:00
if (tag['x-traitTag']) {
continue;
}
2017-10-12 00:01:37 +03:00
tag.operations.push({
...operationInfo,
_$ref: operationPointer,
httpVerb: operationName,
pathParameters: path.parameters || [],
2017-10-12 00:01:37 +03:00
});
}
}
}
return tags;
}
}