redoc/lib/services/schema-helper.service.ts

425 lines
13 KiB
TypeScript
Raw Normal View History

2016-06-22 21:17:48 +03:00
'use strict';
import { JsonPointer } from '../utils/JsonPointer';
2016-10-23 20:36:34 +03:00
import { methods as swaggerMethods, keywordTypes } from '../utils/swagger-defs';
import { WarningsService } from './warnings.service';
2016-08-28 21:46:10 +03:00
import * as slugify from 'slugify';
2016-06-22 21:17:48 +03:00
interface PropertyPreprocessOptions {
childFor: string;
2016-06-30 16:42:36 +03:00
skipReadOnly?: boolean;
2016-06-22 21:17:48 +03:00
}
export interface MenuMethod {
active: boolean;
summary: string;
tag: string;
pointer: string;
2016-11-23 02:23:32 +03:00
operationId: string;
2016-11-24 19:21:49 +03:00
ready: boolean;
}
2016-12-23 01:41:36 +03:00
export interface MenuItem {
2016-07-26 12:03:15 +03:00
id: string;
2016-12-23 01:41:36 +03:00
name: string;
description?: string;
2016-12-23 01:41:36 +03:00
items?: Array<MenuItem>;
parent?: MenuItem;
active?: boolean;
ready?: boolean;
level?: number;
flatIdx?: number;
metadata?: any;
}
2016-11-24 16:29:29 +03:00
// global var for this module
var specMgrInstance;
2016-06-22 21:17:48 +03:00
const injectors = {
notype: {
check: (propertySchema) => !propertySchema.type,
inject: (injectTo, propertySchema, pointer) => {
injectTo.type = SchemaHelper.detectType(propertySchema);
propertySchema.type = injectTo.type;
if (injectTo.type) {
let message = `No "type" specified at "${pointer}". Automatically detected: "${injectTo.type}"`;
WarningsService.warn(message);
}
}
},
2016-06-22 21:17:48 +03:00
general: {
check: () => true,
inject: (injectTo, propertySchema, pointer) => {
injectTo._pointer = propertySchema._pointer || pointer;
injectTo._displayType = propertySchema.type;
if (propertySchema.format) injectTo._displayFormat = `<${propertySchema.format}>`;
if (propertySchema.enum) {
injectTo.enum = propertySchema.enum.map((value) => {
return {val: value, type: typeof value};
});
if (propertySchema.enum && propertySchema.enum.length === 1) {
injectTo._enumItem = propertySchema.enum[0];
injectTo.enum = null;
}
2016-06-22 21:17:48 +03:00
}
}
},
discriminator: {
check: (propertySchema) => propertySchema.discriminator || propertySchema['x-extendedDiscriminator'],
2016-12-02 12:59:29 +03:00
inject: (injectTo, propertySchema = injectTo) => {
2016-06-22 21:17:48 +03:00
injectTo.discriminator = propertySchema.discriminator;
injectTo['x-extendedDiscriminator'] = propertySchema['x-extendedDiscriminator'];
2016-06-22 21:17:48 +03:00
}
},
2016-08-10 14:44:24 +03:00
simpleArray: {
2016-06-22 21:17:48 +03:00
check: (propertySchema) => {
2016-08-10 14:44:24 +03:00
return propertySchema.type === 'array' && !Array.isArray(propertySchema.items);
2016-06-22 21:17:48 +03:00
},
inject: (injectTo, propertySchema = injectTo, propPointer) => {
if (!(SchemaHelper.detectType(propertySchema.items) === 'object')) {
injectTo._isArray = true;
injectTo._pointer = propertySchema.items._pointer
|| JsonPointer.join(propertySchema._pointer || propPointer, ['items']);
2016-06-22 21:17:48 +03:00
SchemaHelper.runInjectors(injectTo, propertySchema.items, propPointer);
} else {
injectors.object.inject(injectTo, propertySchema.items);
}
2016-08-10 15:22:42 +03:00
injectTo._widgetType = 'array';
2016-06-22 21:17:48 +03:00
}
},
2016-08-10 14:44:24 +03:00
tuple: {
check: (propertySchema) => {
return propertySchema.type === 'array' && Array.isArray(propertySchema.items);
},
inject: (injectTo, propertySchema = injectTo, propPointer) => {
injectTo._isTuple = true;
injectTo._displayType = '';
let itemsPtr = JsonPointer.join(propertySchema._pointer || propPointer, ['items']);
for (let i=0; i < propertySchema.items.length; i++) {
let itemSchema = propertySchema.items[i];
itemSchema._pointer = itemSchema._pointer || JsonPointer.join(itemsPtr, [i.toString()]);
}
2016-08-10 15:22:42 +03:00
injectTo._widgetType = 'tuple';
2016-08-10 14:44:24 +03:00
}
},
2016-06-22 21:17:48 +03:00
object: {
check: (propertySchema) => {
2016-08-30 13:52:49 +03:00
return propertySchema.type === 'object' && (propertySchema.properties ||
2016-08-30 20:05:21 +03:00
typeof propertySchema.additionalProperties === 'object');
2016-06-22 21:17:48 +03:00
},
inject: (injectTo, propertySchema = injectTo) => {
let baseName = propertySchema._pointer && JsonPointer.baseName(propertySchema._pointer);
injectTo._displayType = propertySchema.title || baseName || 'object';
2016-08-10 15:22:42 +03:00
injectTo._widgetType = 'object';
2016-06-22 21:17:48 +03:00
}
},
noType: {
check: (propertySchema) => !propertySchema.type,
inject: (injectTo) => {
2016-08-10 15:22:42 +03:00
injectTo._displayType = '< anything >';
2016-06-22 21:17:48 +03:00
injectTo._displayTypeHint = 'This field may contain data of any type';
injectTo.isTrivial = true;
2016-08-10 15:22:42 +03:00
injectTo._widgetType = 'trivial';
2016-11-05 17:25:59 +03:00
injectTo._pointer = undefined;
2016-06-22 21:17:48 +03:00
}
},
simpleType: {
check: (propertySchema) => {
if (propertySchema.type === 'object') {
return (!propertySchema.properties || !Object.keys(propertySchema.properties).length)
&& (typeof propertySchema.additionalProperties !== 'object');
}
return (propertySchema.type !== 'array') && propertySchema.type;
},
inject: (injectTo, propertySchema = injectTo) => {
injectTo.isTrivial = true;
if (injectTo._pointer) {
injectTo._pointer = undefined;
injectTo._displayType = propertySchema.title ?
`${propertySchema.title} (${propertySchema.type})` : propertySchema.type;
}
2016-08-10 15:22:42 +03:00
injectTo._widgetType = 'trivial';
2016-06-22 21:17:48 +03:00
}
},
integer: {
check: (propertySchema) => (propertySchema.type === 'integer' || propertySchema.type === 'number'),
inject: (injectTo, propertySchema = injectTo) => {
var range = '';
2016-10-14 12:34:44 +03:00
if (propertySchema.minimum != undefined && propertySchema.maximum != undefined) {
2016-06-22 21:17:48 +03:00
range += propertySchema.exclusiveMinimum ? '( ' : '[ ';
range += propertySchema.minimum;
range += ' .. ';
range += propertySchema.maximum;
range += propertySchema.exclusiveMaximum ? ' )' : ' ]';
2016-10-14 12:34:44 +03:00
} else if (propertySchema.maximum != undefined) {
2016-06-22 21:17:48 +03:00
range += propertySchema.exclusiveMaximum? '< ' : '<= ';
range += propertySchema.maximum;
2016-10-14 12:34:44 +03:00
} else if (propertySchema.minimum != undefined) {
2016-06-22 21:17:48 +03:00
range += propertySchema.exclusiveMinimum ? '> ' : '>= ';
range += propertySchema.minimum;
}
if (range) {
injectTo._range = range;
}
}
},
string: {
check: propertySchema => (propertySchema.type === 'string'),
inject: (injectTo, propertySchema = injectTo) => {
var range;
2016-10-14 12:34:44 +03:00
if (propertySchema.minLength != undefined && propertySchema.maxLength != undefined) {
2016-06-22 21:17:48 +03:00
range = `[ ${propertySchema.minLength} .. ${propertySchema.maxLength} ]`;
2016-10-14 12:34:44 +03:00
} else if (propertySchema.maxLength != undefined) {
2016-06-22 21:17:48 +03:00
range = '<= ' + propertySchema.maxLength;
2016-10-14 12:34:44 +03:00
} else if (propertySchema.minLength != undefined) {
2016-06-22 21:17:48 +03:00
range = '>= ' + propertySchema.minLength;
}
if (range) {
injectTo._range = range + ' characters';
}
}
},
file: {
check: propertySchema => (propertySchema.type === 'file'),
2016-12-02 12:59:29 +03:00
inject: (injectTo, propertySchema = injectTo, _, hostPointer) => {
2016-06-22 21:17:48 +03:00
injectTo.isFile = true;
let parentPtr;
if (propertySchema.in === 'formData') {
parentPtr = JsonPointer.dirName(hostPointer, 1);
} else {
parentPtr = JsonPointer.dirName(hostPointer, 3);
}
2016-11-24 16:29:29 +03:00
let parentParam = specMgrInstance.byPointer(parentPtr);
let root =specMgrInstance.schema;
2016-06-22 21:17:48 +03:00
injectTo._produces = parentParam && parentParam.produces || root.produces;
injectTo._consumes = parentParam && parentParam.consumes || root.consumes;
2016-08-10 15:22:42 +03:00
injectTo._widgetType = 'file';
2016-06-22 21:17:48 +03:00
}
}
};
export class SchemaHelper {
2016-11-24 16:29:29 +03:00
static setSpecManager(specMgr) {
specMgrInstance = specMgr;
}
2016-06-25 13:02:13 +03:00
static preprocess(schema, pointer, hostPointer?) {
2016-06-22 21:17:48 +03:00
//propertySchema = Object.assign({}, propertySchema);
if (schema['x-redoc-schema-precompiled']) {
return schema;
}
SchemaHelper.runInjectors(schema, schema, pointer, hostPointer);
schema['x-redoc-schema-precompiled'] = true;
return schema;
}
static runInjectors(injectTo, schema, pointer, hostPointer?) {
for (var injName of Object.keys(injectors)) {
let injector = injectors[injName];
if (injector.check(schema)) {
injector.inject(injectTo, schema, pointer, hostPointer);
}
}
}
static preprocessProperties(schema:any, pointer:string, opts: PropertyPreprocessOptions) {
let requiredMap = {};
if (schema.required) {
schema.required.forEach(prop => requiredMap[prop] = true);
}
2016-12-02 12:59:29 +03:00
let props = schema.properties && Object.keys(schema.properties).map(propName => {
2016-06-25 13:02:13 +03:00
let propertySchema = Object.assign({}, schema.properties[propName]);
2016-06-22 21:17:48 +03:00
let propPointer = propertySchema._pointer ||
JsonPointer.join(pointer, ['properties', propName]);
2016-06-25 13:02:13 +03:00
propertySchema = SchemaHelper.preprocess(propertySchema, propPointer);
propertySchema._name = propName;
2016-06-22 21:17:48 +03:00
// stop endless discriminator recursion
if (propertySchema._pointer === opts.childFor) {
propertySchema._pointer = null;
}
2016-06-25 13:02:13 +03:00
propertySchema._required = !!requiredMap[propName];
propertySchema.isDiscriminator = (schema.discriminator === propName
|| schema['x-extendedDiscriminator'] === propName);
2016-06-22 21:17:48 +03:00
return propertySchema;
});
props = props || [];
2016-06-29 22:29:05 +03:00
if (schema.additionalProperties && (typeof schema.additionalProperties === 'object')) {
2016-06-22 21:17:48 +03:00
let propsSchema = SchemaHelper.preprocessAdditionalProperties(schema, pointer);
propsSchema._additional = true;
props.push(propsSchema);
}
// filter readOnly props for request schemas
if (opts.skipReadOnly) {
props = props.filter(prop => !prop.readOnly);
}
schema._properties = props;
}
static preprocessAdditionalProperties(schema:any, pointer:string) {
var addProps = schema.additionalProperties;
let ptr = addProps._pointer || JsonPointer.join(pointer, ['additionalProperties']);
2016-06-25 13:02:13 +03:00
let res = SchemaHelper.preprocess(addProps, ptr);
res._name = '<Additional Properties> *';
return res;
2016-06-22 21:17:48 +03:00
}
static unwrapArray(schema, pointer) {
var res = schema;
2016-08-10 14:44:24 +03:00
if (schema && schema.type === 'array' && !Array.isArray(schema.items)) {
let items = schema.items = schema.items || {};
let ptr = items._pointer || JsonPointer.join(pointer, ['items']);
2016-11-28 21:03:01 +03:00
res = Object.assign({}, items);
2016-06-22 21:17:48 +03:00
res._isArray = true;
res._pointer = ptr;
res = SchemaHelper.unwrapArray(res, ptr);
}
return res;
}
static methodSummary(method) {
return method.summary || method.operationId ||
(method.description && method.description.substring(0, 50)) || '<no description>';
}
static detectType(schema) {
if (schema.type) return schema.type;
let keywords = Object.keys(keywordTypes);
for (var i=0; i < keywords.length; i++) {
let keyword = keywords[i];
let type = keywordTypes[keyword];
if (schema[keyword]) {
return type;
}
}
}
2016-12-23 01:41:36 +03:00
static getTags(schema) {
let tags = {};
for (let tag of schema.tags || []) {
2016-12-23 01:41:36 +03:00
tags[tag.name] = tag;
tag.methods = [];
}
let paths = schema.paths;
for (let path of Object.keys(paths)) {
let methods = Object.keys(paths[path]).filter((k) => swaggerMethods.has(k));
for (let method of methods) {
let methodInfo = paths[path][method];
2016-12-23 01:41:36 +03:00
let methodTags = methodInfo.tags;
2016-12-23 01:41:36 +03:00
if (!(methodTags && methodTags.length)) {
methodTags = [''];
}
2016-12-23 01:41:36 +03:00
let methodPointer = JsonPointer.compile([path, method]);
for (let tagName of methodTags) {
let tag = tags[tagName];
if (!tag) {
tag = {
name: tagName,
};
2016-12-23 01:41:36 +03:00
tags[tagName] = tag;
}
if (tag['x-traitTag']) continue;
if (!tag.methods) tag.methods = [];
tag.methods.push(methodInfo);
methodInfo._pointer = methodPointer;
}
}
}
return Object.keys(tags).map(k => tags[k]);
}
static buildMenuTree(schema):MenuItem[] {
let tags = SchemaHelper.getTags(schema);
let menu = [];
// markdown menu items
for (let header of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) {
let id = 'section/' + slugify(header);
let item = {
name: header,
id: id
}
menu.push(item);
}
// tag menu items
for (let tag of tags || []) {
let id = 'tag/' + slugify(tag.name);
let item:MenuItem;
let items:MenuItem[];
// don't put empty tag into menu, instead put all methods
if (tag.name !== '') {
item = {
name: tag['x-displayName'] || tag.name,
id: id,
description: tag.description,
metadata: { type: 'tag' }
};
if (tag.methods && tag.methods.length) {
item.items = items = [];
}
} else {
item = null;
items = menu;
}
if (items) {
for (let method of tag.methods) {
let subItem = {
name: SchemaHelper.methodSummary(method),
id: method._pointer,
description: method.description,
metadata: {
type: 'method',
pointer: '/paths' + method._pointer,
operationId: method.operationId
},
parent: item
}
2016-12-23 01:41:36 +03:00
items.push(subItem);
}
}
2016-12-23 01:41:36 +03:00
if (item) menu.push(item);
}
2016-12-23 01:41:36 +03:00
return menu;
}
static flatMenu(menu: MenuItem[]):MenuItem[] {
let res = [];
let level = 0;
let recursive = function(items) {
for (let item of items) {
res.push(item);
item.level = item.level || level;
item.flatIdx = res.length - 1;
if (item.items) {
level++;
recursive(item.items);
level--;
}
}
}
recursive(menu);
return res;
}
2016-06-22 21:17:48 +03:00
}