2015-10-07 12:47:57 +03:00
|
|
|
'use strict';
|
2017-04-18 16:46:51 +03:00
|
|
|
import { Injectable } from '@angular/core';
|
2016-08-28 21:46:10 +03:00
|
|
|
import * as JsonSchemaRefParser from 'json-schema-ref-parser';
|
|
|
|
import { JsonPointer } from './JsonPointer';
|
2016-09-12 22:48:44 +03:00
|
|
|
import { parse as urlParse, resolve as urlResolve } from 'url';
|
2016-10-23 20:18:42 +03:00
|
|
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
2015-10-07 12:47:57 +03:00
|
|
|
|
2016-10-31 11:15:04 +03:00
|
|
|
import { MdRenderer } from './md-renderer';
|
2016-10-30 18:56:24 +03:00
|
|
|
|
2017-01-30 17:11:14 +03:00
|
|
|
import { SwaggerOperation, SwaggerParameter } from './swagger-typings';
|
2017-02-14 11:49:42 +03:00
|
|
|
import { snapshot } from './helpers';
|
2017-04-18 16:46:51 +03:00
|
|
|
import { OptionsService, Options } from '../services/options.service';
|
2017-03-09 20:56:50 +03:00
|
|
|
import { WarningsService } from '../services/warnings.service';
|
2017-01-30 17:11:14 +03:00
|
|
|
|
2016-12-28 15:04:57 +03:00
|
|
|
function getDiscriminator(obj) {
|
|
|
|
return obj.discriminator || obj['x-extendedDiscriminator'];
|
|
|
|
}
|
|
|
|
|
2017-01-26 19:21:24 +03:00
|
|
|
export interface DescendantInfo {
|
|
|
|
$ref: string;
|
|
|
|
name: string;
|
|
|
|
active?: boolean;
|
2017-01-28 16:57:22 +03:00
|
|
|
idx?: number;
|
2017-01-26 19:21:24 +03:00
|
|
|
}
|
|
|
|
|
2017-04-18 16:46:51 +03:00
|
|
|
@Injectable()
|
2016-06-22 21:17:48 +03:00
|
|
|
export class SpecManager {
|
2016-07-26 14:51:59 +03:00
|
|
|
public _schema: any = {};
|
2016-06-13 20:54:24 +03:00
|
|
|
public apiUrl: string;
|
2017-02-26 00:38:19 +03:00
|
|
|
public apiProtocol: string;
|
|
|
|
public swagger: string;
|
2016-11-01 21:40:59 +03:00
|
|
|
public basePath: string;
|
2016-10-23 20:18:42 +03:00
|
|
|
|
|
|
|
public spec = new BehaviorSubject<any|null>(null);
|
2017-04-23 15:22:04 +03:00
|
|
|
public specUrl: string;
|
2016-11-08 12:02:36 +03:00
|
|
|
private parser: any;
|
2017-04-18 16:46:51 +03:00
|
|
|
private options: Options;
|
|
|
|
|
|
|
|
constructor(optionsService: OptionsService) {
|
|
|
|
this.options = optionsService.options;
|
|
|
|
}
|
2016-06-12 20:44:34 +03:00
|
|
|
|
2016-10-23 20:18:42 +03:00
|
|
|
load(urlOrObject: string|Object) {
|
2015-10-07 12:47:57 +03:00
|
|
|
let promise = new Promise((resolve, reject) => {
|
2016-11-08 12:02:36 +03:00
|
|
|
this.parser = new JsonSchemaRefParser();
|
|
|
|
this.parser.bundle(urlOrObject, {http: {withCredentials: false}})
|
2016-06-22 21:17:48 +03:00
|
|
|
.then(schema => {
|
2016-10-23 20:18:42 +03:00
|
|
|
if (typeof urlOrObject === 'string') {
|
2017-04-23 15:22:04 +03:00
|
|
|
this.specUrl = urlOrObject;
|
2016-10-23 20:18:42 +03:00
|
|
|
}
|
2017-02-14 11:49:42 +03:00
|
|
|
this._schema = snapshot(schema);
|
2016-10-14 11:44:18 +03:00
|
|
|
try {
|
2016-06-22 21:17:48 +03:00
|
|
|
this.init();
|
2016-11-24 16:29:29 +03:00
|
|
|
this.spec.next(this._schema);
|
2017-03-29 13:29:54 +03:00
|
|
|
resolve(this._schema);
|
2016-10-14 11:44:18 +03:00
|
|
|
} catch(err) {
|
|
|
|
reject(err);
|
|
|
|
}
|
2016-06-22 21:17:48 +03:00
|
|
|
}, err => reject(err));
|
2015-10-07 12:47:57 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
return promise;
|
|
|
|
}
|
|
|
|
|
2015-11-17 01:03:09 +03:00
|
|
|
/* calculate common used values */
|
|
|
|
init() {
|
2017-04-23 15:22:04 +03:00
|
|
|
let urlParts = this.specUrl ? urlParse(urlResolve(window.location.href, this.specUrl)) : {};
|
2016-08-01 03:26:10 +03:00
|
|
|
let schemes = this._schema.schemes;
|
2016-07-26 14:51:59 +03:00
|
|
|
let protocol;
|
2016-08-01 03:26:10 +03:00
|
|
|
if (!schemes || !schemes.length) {
|
|
|
|
// url parser incudles ':' in protocol so remove it
|
|
|
|
protocol = urlParts.protocol ? urlParts.protocol.slice(0, -1) : 'http';
|
2016-07-26 14:51:59 +03:00
|
|
|
} else {
|
2016-08-01 03:26:10 +03:00
|
|
|
protocol = schemes[0];
|
|
|
|
if (protocol === 'http' && schemes.indexOf('https') >= 0) {
|
2016-07-26 14:51:59 +03:00
|
|
|
protocol = 'https';
|
|
|
|
}
|
|
|
|
}
|
2016-08-01 03:26:10 +03:00
|
|
|
|
|
|
|
let host = this._schema.host || urlParts.host;
|
2017-02-14 10:49:58 +03:00
|
|
|
this.basePath = this._schema.basePath || '';
|
2016-11-01 21:40:59 +03:00
|
|
|
this.apiUrl = protocol + '://' + host + this.basePath;
|
2017-02-26 00:38:19 +03:00
|
|
|
this.apiProtocol = protocol;
|
2015-11-29 18:14:18 +03:00
|
|
|
if (this.apiUrl.endsWith('/')) {
|
|
|
|
this.apiUrl = this.apiUrl.substr(0, this.apiUrl.length - 1);
|
|
|
|
}
|
2016-07-21 13:35:27 +03:00
|
|
|
|
|
|
|
this.preprocess();
|
|
|
|
}
|
|
|
|
|
|
|
|
preprocess() {
|
2016-10-30 18:56:24 +03:00
|
|
|
let mdRender = new MdRenderer();
|
2017-02-26 00:38:19 +03:00
|
|
|
if (!this._schema.info) {
|
2017-02-26 01:44:21 +03:00
|
|
|
throw Error('Specification Error: Required field "info" is not specified at the top level of the specification');
|
2017-02-26 00:38:19 +03:00
|
|
|
}
|
2016-10-30 18:56:24 +03:00
|
|
|
if (!this._schema.info.description) this._schema.info.description = '';
|
2017-04-18 16:46:51 +03:00
|
|
|
if (this._schema.securityDefinitions && !this.options.noAutoAuth) {
|
2017-06-26 12:52:33 +03:00
|
|
|
let SecurityDefinitions =
|
|
|
|
require('../components/SecurityDefinitions/security-definitions').SecurityDefinitions;
|
2016-10-31 11:15:04 +03:00
|
|
|
mdRender.addPreprocessor(SecurityDefinitions.insertTagIntoDescription);
|
|
|
|
}
|
2016-10-30 18:56:24 +03:00
|
|
|
this._schema.info['x-redoc-html-description'] = mdRender.renderMd(this._schema.info.description);
|
2017-01-30 18:21:12 +03:00
|
|
|
this._schema.info['x-redoc-markdown-headers'] = mdRender.headings;
|
2015-11-17 01:03:09 +03:00
|
|
|
}
|
|
|
|
|
2015-10-07 12:47:57 +03:00
|
|
|
get schema() {
|
|
|
|
return this._schema;
|
|
|
|
}
|
|
|
|
|
2016-10-23 20:18:42 +03:00
|
|
|
set schema(val:any) {
|
|
|
|
this._schema = val;
|
|
|
|
this.spec.next(this._schema);
|
|
|
|
}
|
|
|
|
|
2015-10-08 23:21:51 +03:00
|
|
|
byPointer(pointer) {
|
2015-10-10 16:01:41 +03:00
|
|
|
let res = null;
|
2016-11-08 12:02:36 +03:00
|
|
|
if (pointer == undefined) return null;
|
2015-10-10 16:01:41 +03:00
|
|
|
try {
|
2016-01-17 00:25:12 +03:00
|
|
|
res = JsonPointer.get(this._schema, decodeURIComponent(pointer));
|
2016-11-08 12:02:36 +03:00
|
|
|
} catch(e) {
|
|
|
|
// if resolved from outer files simple jsonpointer.get fails to get correct schema
|
|
|
|
if (pointer.charAt(0) !== '#') pointer = '#' + pointer;
|
|
|
|
try {
|
|
|
|
res = this.parser.$refs.get(decodeURIComponent(pointer));
|
|
|
|
} catch(e) { /* skip */ }
|
|
|
|
}
|
2015-10-10 16:01:41 +03:00
|
|
|
return res;
|
2015-10-07 12:47:57 +03:00
|
|
|
}
|
2015-10-08 23:21:51 +03:00
|
|
|
|
2015-10-25 14:26:38 +03:00
|
|
|
resolveRefs(obj) {
|
|
|
|
Object.keys(obj).forEach(key => {
|
|
|
|
if (obj[key].$ref) {
|
2015-12-12 18:29:50 +03:00
|
|
|
let resolved = this.byPointer(obj[key].$ref);
|
|
|
|
resolved._pointer = obj[key].$ref;
|
|
|
|
obj[key] = resolved;
|
2015-10-25 14:26:38 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
2017-03-30 15:17:08 +03:00
|
|
|
getOperationParams(operationPtr:string):SwaggerParameter[] {
|
2015-10-25 14:26:38 +03:00
|
|
|
/* inject JsonPointer into array elements */
|
2017-01-30 17:11:14 +03:00
|
|
|
function injectPointers(array:SwaggerParameter[], root) {
|
2015-12-14 16:53:22 +03:00
|
|
|
if (!Array.isArray(array)) {
|
|
|
|
throw new Error(`parameters must be an array. Got ${typeof array} at ${root}`);
|
|
|
|
}
|
2015-10-25 14:26:38 +03:00
|
|
|
return array.map((element, idx) => {
|
|
|
|
element._pointer = JsonPointer.join(root, idx);
|
|
|
|
return element;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-12-12 18:29:50 +03:00
|
|
|
// accept pointer directly to parameters as well
|
2017-03-30 15:17:08 +03:00
|
|
|
if (JsonPointer.baseName(operationPtr) === 'parameters') {
|
|
|
|
operationPtr = JsonPointer.dirName(operationPtr);
|
2015-10-25 14:26:38 +03:00
|
|
|
}
|
|
|
|
|
2015-12-12 18:29:50 +03:00
|
|
|
//get path params
|
2017-03-30 15:17:08 +03:00
|
|
|
let pathParamsPtr = JsonPointer.join(JsonPointer.dirName(operationPtr), ['parameters']);
|
2017-01-30 17:11:14 +03:00
|
|
|
let pathParams:SwaggerParameter[] = this.byPointer(pathParamsPtr) || [];
|
2015-12-12 18:29:50 +03:00
|
|
|
|
2017-03-30 15:17:08 +03:00
|
|
|
let operationParamsPtr = JsonPointer.join(operationPtr, ['parameters']);
|
|
|
|
let operationParams:SwaggerParameter[] = this.byPointer(operationParamsPtr) || [];
|
2015-12-12 18:29:50 +03:00
|
|
|
pathParams = injectPointers(pathParams, pathParamsPtr);
|
2017-03-30 15:17:08 +03:00
|
|
|
operationParams = injectPointers(operationParams, operationParamsPtr);
|
2015-12-12 18:29:50 +03:00
|
|
|
|
2017-01-30 17:11:14 +03:00
|
|
|
// resolve references
|
2017-03-30 15:17:08 +03:00
|
|
|
operationParams = this.resolveRefs(operationParams);
|
2017-01-30 17:11:14 +03:00
|
|
|
pathParams = this.resolveRefs(pathParams);
|
2017-03-30 15:17:08 +03:00
|
|
|
return operationParams.concat(pathParams);
|
2015-10-25 14:26:38 +03:00
|
|
|
}
|
|
|
|
|
2015-11-17 02:34:13 +03:00
|
|
|
getTagsMap() {
|
|
|
|
let tags = this._schema.tags || [];
|
|
|
|
var tagsMap = {};
|
|
|
|
for (let tag of tags) {
|
|
|
|
tagsMap[tag.name] = {
|
|
|
|
description: tag.description,
|
2015-12-12 14:49:22 +03:00
|
|
|
'x-traitTag': tag['x-traitTag'] || false
|
2015-11-17 02:34:13 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return tagsMap;
|
|
|
|
}
|
|
|
|
|
2017-02-26 00:38:19 +03:00
|
|
|
findDerivedDefinitions(defPointer: string, schema?: any): DescendantInfo[] {
|
2016-12-28 15:04:57 +03:00
|
|
|
let definition = schema || this.byPointer(defPointer);
|
2016-01-15 23:42:55 +03:00
|
|
|
if (!definition) throw new Error(`Can't load schema at ${defPointer}`);
|
2016-09-28 09:36:21 +03:00
|
|
|
if (!definition.discriminator && !definition['x-extendedDiscriminator']) return [];
|
2016-01-09 17:52:24 +03:00
|
|
|
|
|
|
|
let globalDefs = this._schema.definitions || {};
|
2017-01-28 14:03:51 +03:00
|
|
|
let res:DescendantInfo[] = [];
|
2017-03-10 01:04:15 +03:00
|
|
|
|
|
|
|
|
|
|
|
// from the spec: When used, the value MUST be the name of this schema or any schema that inherits it.
|
|
|
|
// but most of people use it as an abstract class so here is workaround to allow using it other way
|
|
|
|
// check if parent definition name is in the enum of possible values
|
|
|
|
if (definition.discriminator) {
|
|
|
|
let prop = definition.properties[definition.discriminator];
|
2017-03-29 13:30:48 +03:00
|
|
|
if (prop && prop.enum && prop.enum.indexOf(JsonPointer.baseName(defPointer)) > -1) {
|
2017-03-10 01:04:15 +03:00
|
|
|
res.push({
|
|
|
|
name: JsonPointer.baseName(defPointer),
|
|
|
|
$ref: defPointer
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-28 09:36:21 +03:00
|
|
|
let extendedDiscriminatorProp = definition['x-extendedDiscriminator'];
|
2017-03-09 20:59:49 +03:00
|
|
|
|
|
|
|
let pointers;
|
|
|
|
if (definition['x-derived-from']) {
|
|
|
|
// support inherited discriminator o_O
|
|
|
|
let derivedDiscriminator = definition['x-derived-from'].filter(ptr => {
|
|
|
|
if (!ptr) return false;
|
|
|
|
let def = this.byPointer(ptr);
|
|
|
|
return def && def.discriminator;
|
|
|
|
});
|
|
|
|
pointers = [defPointer, ...derivedDiscriminator];
|
|
|
|
} else {
|
|
|
|
pointers = [defPointer];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-01-09 17:52:24 +03:00
|
|
|
for (let defName of Object.keys(globalDefs)) {
|
2016-09-28 09:36:21 +03:00
|
|
|
let def = globalDefs[defName];
|
|
|
|
if (!def.allOf &&
|
|
|
|
!def['x-derived-from']) continue;
|
|
|
|
let subTypes = def['x-derived-from'] ||
|
|
|
|
def.allOf.map(subType => subType._pointer || subType.$ref);
|
2016-12-28 15:04:57 +03:00
|
|
|
|
|
|
|
let idx = -1;
|
|
|
|
|
|
|
|
for (let ptr of pointers) {
|
|
|
|
idx = subTypes.findIndex(ref => ptr && ref === ptr);
|
|
|
|
if (idx >= 0) break;
|
|
|
|
}
|
|
|
|
|
2016-01-09 17:52:24 +03:00
|
|
|
if (idx < 0) continue;
|
|
|
|
|
2017-03-09 20:56:50 +03:00
|
|
|
let derivedName;
|
2016-09-28 09:36:21 +03:00
|
|
|
if (extendedDiscriminatorProp) {
|
2017-03-09 20:56:50 +03:00
|
|
|
let subDefs = def.allOf || [];
|
|
|
|
for (let def of subDefs) {
|
|
|
|
let prop = def.properties && def.properties[extendedDiscriminatorProp];
|
|
|
|
if (prop && prop.enum && prop.enum.length === 1) {
|
|
|
|
derivedName = prop.enum[0];
|
|
|
|
break;
|
|
|
|
}
|
2016-09-28 09:36:21 +03:00
|
|
|
}
|
2017-03-10 01:04:15 +03:00
|
|
|
if (derivedName == undefined) {
|
2017-03-09 20:56:50 +03:00
|
|
|
WarningsService.warn(`Incorrect usage of x-extendedDiscriminator at ${defPointer}: `
|
|
|
|
+ `can't find corresponding enum with single value in definition "${defName}"`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
derivedName = defName;
|
2016-09-28 09:36:21 +03:00
|
|
|
}
|
|
|
|
|
2017-01-28 16:57:22 +03:00
|
|
|
res.push({name: derivedName, $ref: `#/definitions/${defName}`});
|
2016-01-09 17:52:24 +03:00
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2017-01-26 19:21:24 +03:00
|
|
|
getDescendant(descendant:DescendantInfo, componentSchema:any) {
|
2016-12-28 15:04:57 +03:00
|
|
|
let res;
|
|
|
|
if (!getDiscriminator(componentSchema) && componentSchema.allOf) {
|
|
|
|
// discriminator inherited from parents
|
|
|
|
// only one discriminator and only one level of inheritence is supported at the moment
|
|
|
|
res = Object.assign({}, componentSchema);
|
|
|
|
let idx = res.allOf.findIndex(subSpec => !!getDiscriminator(subSpec));
|
|
|
|
res.allOf[idx] = this.byPointer(descendant.$ref);
|
|
|
|
} else {
|
|
|
|
// this.pointer = activeDescendant.$ref;
|
|
|
|
res = this.byPointer(descendant.$ref);
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2015-10-07 12:47:57 +03:00
|
|
|
}
|