'use strict'; import * as JsonSchemaRefParser from 'json-schema-ref-parser'; import { JsonPointer } from './JsonPointer'; import { parse as urlParse, resolve as urlResolve } from 'url'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { MdRenderer } from './md-renderer'; import { SwaggerOperation, SwaggerParameter } from './swagger-typings'; import { snapshot } from './helpers'; import { WarningsService } from '../services/warnings.service'; function getDiscriminator(obj) { return obj.discriminator || obj['x-extendedDiscriminator']; } export interface DescendantInfo { $ref: string; name: string; active?: boolean; idx?: number; } export class SpecManager { public _schema: any = {}; public apiUrl: string; public apiProtocol: string; public swagger: string; public basePath: string; public spec = new BehaviorSubject(null); public _specUrl: string; private parser: any; load(urlOrObject: string|Object) { let promise = new Promise((resolve, reject) => { this.parser = new JsonSchemaRefParser(); this.parser.bundle(urlOrObject, {http: {withCredentials: false}}) .then(schema => { if (typeof urlOrObject === 'string') { this._specUrl = urlOrObject; } this._schema = snapshot(schema); try { this.init(); this.spec.next(this._schema); resolve(this._schema); } catch(err) { reject(err); } }, err => reject(err)); }); return promise; } /* calculate common used values */ init() { let urlParts = this._specUrl ? urlParse(urlResolve(window.location.href, this._specUrl)) : {}; let schemes = this._schema.schemes; let protocol; if (!schemes || !schemes.length) { // url parser incudles ':' in protocol so remove it protocol = urlParts.protocol ? urlParts.protocol.slice(0, -1) : 'http'; } else { protocol = schemes[0]; if (protocol === 'http' && schemes.indexOf('https') >= 0) { protocol = 'https'; } } let host = this._schema.host || urlParts.host; this.basePath = this._schema.basePath || ''; this.apiUrl = protocol + '://' + host + this.basePath; this.apiProtocol = protocol; if (this.apiUrl.endsWith('/')) { this.apiUrl = this.apiUrl.substr(0, this.apiUrl.length - 1); } this.preprocess(); } preprocess() { let mdRender = new MdRenderer(); if (!this._schema.info) { throw Error('Specification Error: Required field "info" is not specified at the top level of the specification'); } if (!this._schema.info.description) this._schema.info.description = ''; if (this._schema.securityDefinitions) { let SecurityDefinitions = require('../components/').SecurityDefinitions; mdRender.addPreprocessor(SecurityDefinitions.insertTagIntoDescription); } this._schema.info['x-redoc-html-description'] = mdRender.renderMd(this._schema.info.description); this._schema.info['x-redoc-markdown-headers'] = mdRender.headings; } get schema() { return this._schema; } set schema(val:any) { this._schema = val; this.spec.next(this._schema); } byPointer(pointer) { let res = null; if (pointer == undefined) return null; try { res = JsonPointer.get(this._schema, decodeURIComponent(pointer)); } 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 */ } } return res; } resolveRefs(obj) { Object.keys(obj).forEach(key => { if (obj[key].$ref) { let resolved = this.byPointer(obj[key].$ref); resolved._pointer = obj[key].$ref; obj[key] = resolved; } }); return obj; } getMethodParams(methodPtr:string):SwaggerParameter[] { /* inject JsonPointer into array elements */ function injectPointers(array:SwaggerParameter[], root) { if (!Array.isArray(array)) { throw new Error(`parameters must be an array. Got ${typeof array} at ${root}`); } return array.map((element, idx) => { element._pointer = JsonPointer.join(root, idx); return element; }); } // accept pointer directly to parameters as well if (JsonPointer.baseName(methodPtr) === 'parameters') { methodPtr = JsonPointer.dirName(methodPtr); } //get path params let pathParamsPtr = JsonPointer.join(JsonPointer.dirName(methodPtr), ['parameters']); let pathParams:SwaggerParameter[] = this.byPointer(pathParamsPtr) || []; let methodParamsPtr = JsonPointer.join(methodPtr, ['parameters']); let methodParams:SwaggerParameter[] = this.byPointer(methodParamsPtr) || []; pathParams = injectPointers(pathParams, pathParamsPtr); methodParams = injectPointers(methodParams, methodParamsPtr); // resolve references methodParams = this.resolveRefs(methodParams); pathParams = this.resolveRefs(pathParams); return methodParams.concat(pathParams); } getTagsMap() { let tags = this._schema.tags || []; var tagsMap = {}; for (let tag of tags) { tagsMap[tag.name] = { description: tag.description, 'x-traitTag': tag['x-traitTag'] || false }; } return tagsMap; } findDerivedDefinitions(defPointer: string, schema?: any): DescendantInfo[] { let definition = schema || this.byPointer(defPointer); if (!definition) throw new Error(`Can't load schema at ${defPointer}`); if (!definition.discriminator && !definition['x-extendedDiscriminator']) return []; let globalDefs = this._schema.definitions || {}; let res:DescendantInfo[] = []; // 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]; if (prop && prop.enum && prop.enum.indexOf(JsonPointer.baseName(defPointer)) > -1) { res.push({ name: JsonPointer.baseName(defPointer), $ref: defPointer }); } } let extendedDiscriminatorProp = definition['x-extendedDiscriminator']; 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]; } for (let defName of Object.keys(globalDefs)) { 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); let idx = -1; for (let ptr of pointers) { idx = subTypes.findIndex(ref => ptr && ref === ptr); if (idx >= 0) break; } if (idx < 0) continue; let derivedName; if (extendedDiscriminatorProp) { 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; } } if (derivedName == undefined) { WarningsService.warn(`Incorrect usage of x-extendedDiscriminator at ${defPointer}: ` + `can't find corresponding enum with single value in definition "${defName}"`); continue; } } else { derivedName = defName; } res.push({name: derivedName, $ref: `#/definitions/${defName}`}); } return res; } getDescendant(descendant:DescendantInfo, componentSchema:any) { 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; } }