2017-11-14 18:46:50 +03:00
|
|
|
import { observable } from 'mobx';
|
2017-10-12 00:01:37 +03:00
|
|
|
import { resolve as urlResolve } from 'url';
|
|
|
|
|
|
|
|
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
|
|
|
|
|
|
|
|
import { JsonPointer } from '../utils/JsonPointer';
|
|
|
|
import { isNamedDefinition } from '../utils/openapi';
|
2017-11-21 17:33:22 +03:00
|
|
|
import { COMPONENT_REGEXP, buildComponentComment } from './MarkdownRenderer';
|
|
|
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
2017-12-15 13:17:14 +03:00
|
|
|
import { appendToMdHeading } from '../utils/';
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2017-12-07 19:38:49 +03:00
|
|
|
export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
|
2017-10-12 00:01:37 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper class to keep track of visited references to avoid
|
|
|
|
* endless recursion because of circular refs
|
|
|
|
*/
|
|
|
|
class RefCounter {
|
|
|
|
public _counter = {};
|
|
|
|
|
|
|
|
reset(): void {
|
|
|
|
this._counter = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
visit(ref: string): void {
|
|
|
|
this._counter[ref] = this._counter[ref] ? this._counter[ref] + 1 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
exit(ref: string): void {
|
|
|
|
this._counter[ref] = this._counter[ref] && this._counter[ref] - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
visited(ref: string): boolean {
|
|
|
|
return !!this._counter[ref];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads and keeps spec. Provides raw spec operations
|
|
|
|
*/
|
|
|
|
export class OpenAPIParser {
|
|
|
|
@observable specUrl: string;
|
2017-11-14 18:46:50 +03:00
|
|
|
@observable.ref spec: OpenAPISpec;
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2017-11-21 17:33:22 +03:00
|
|
|
constructor(
|
|
|
|
spec: OpenAPISpec,
|
|
|
|
specUrl: string | undefined,
|
|
|
|
private options: RedocNormalizedOptions,
|
|
|
|
) {
|
2017-11-14 18:46:50 +03:00
|
|
|
this.validate(spec);
|
2017-11-21 17:33:22 +03:00
|
|
|
this.preprocess(spec);
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2017-11-14 18:46:50 +03:00
|
|
|
this.spec = spec;
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2017-11-14 18:46:50 +03:00
|
|
|
if (typeof specUrl === 'string') {
|
|
|
|
this.specUrl = urlResolve(window.location.href, specUrl);
|
2017-10-12 00:01:37 +03:00
|
|
|
} else {
|
|
|
|
this.specUrl = window.location.href;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-14 18:46:50 +03:00
|
|
|
private _refCounter: RefCounter = new RefCounter();
|
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
validate(spec: any) {
|
|
|
|
if (spec.openapi === undefined) {
|
|
|
|
throw new Error('Document must be valid OpenAPI 3.0.0 definition');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-21 17:33:22 +03:00
|
|
|
preprocess(spec: OpenAPISpec) {
|
2017-11-24 12:49:32 +03:00
|
|
|
if (
|
|
|
|
!this.options.noAutoAuth &&
|
|
|
|
spec.info &&
|
|
|
|
spec.components &&
|
|
|
|
spec.components.securitySchemes
|
|
|
|
) {
|
2017-11-21 17:33:22 +03:00
|
|
|
// Automatically inject Authentication section with SecurityDefinitions component
|
|
|
|
const description = spec.info.description || '';
|
|
|
|
const securityRegexp = new RegExp(
|
|
|
|
COMPONENT_REGEXP.replace('{component}', '<security-definitions>'),
|
|
|
|
'gmi',
|
|
|
|
);
|
|
|
|
if (!securityRegexp.test(description)) {
|
|
|
|
const comment = buildComponentComment('security-definitions');
|
|
|
|
spec.info.description = appendToMdHeading(description, 'Authentication', comment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
/**
|
|
|
|
* get spec part by JsonPointer ($ref)
|
|
|
|
*/
|
|
|
|
byRef = <T extends any = any>(ref: string): T | undefined => {
|
|
|
|
let res;
|
|
|
|
if (this.spec === undefined) return;
|
2017-11-14 18:46:50 +03:00
|
|
|
if (ref.charAt(0) !== '#') ref = '#' + ref;
|
2017-12-07 14:54:34 +03:00
|
|
|
ref = decodeURIComponent(ref);
|
2017-10-12 00:01:37 +03:00
|
|
|
try {
|
2017-12-07 14:54:34 +03:00
|
|
|
res = JsonPointer.get(this.spec, ref);
|
2017-10-12 00:01:37 +03:00
|
|
|
} catch (e) {
|
2017-11-14 18:46:50 +03:00
|
|
|
// do nothing
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
return res;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* checks if the objectt is OpenAPI reference (containts $ref property)
|
|
|
|
*/
|
|
|
|
isRef(obj: any): obj is OpenAPIRef {
|
2017-12-07 14:54:34 +03:00
|
|
|
if (!obj) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
return obj.$ref !== undefined && obj.$ref !== null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* resets visited enpoints. should be run after
|
|
|
|
*/
|
|
|
|
resetVisited() {
|
2017-11-24 12:45:31 +03:00
|
|
|
if (__DEV__) {
|
|
|
|
// check in dev mode
|
|
|
|
for (let k in this._refCounter._counter) {
|
|
|
|
if (this._refCounter._counter[k] > 0) {
|
|
|
|
console.warn('Not exited reference: ' + k);
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this._refCounter = new RefCounter();
|
|
|
|
}
|
|
|
|
|
|
|
|
exitRef<T>(ref: Referenced<T>) {
|
|
|
|
if (!this.isRef(ref)) return;
|
|
|
|
this._refCounter.exit(ref.$ref);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolve given reference object or return as is if it is not a reference
|
|
|
|
* @param obj object to dereference
|
|
|
|
* @param forceCircular whether to dereference even if it is cirular ref
|
|
|
|
*/
|
|
|
|
deref<T extends object>(obj: OpenAPIRef | T, forceCircular: boolean = false): T {
|
|
|
|
if (this.isRef(obj)) {
|
|
|
|
const resolved = this.byRef<T>(obj.$ref)!;
|
2017-11-24 12:45:31 +03:00
|
|
|
const visited = this._refCounter.visited(obj.$ref);
|
|
|
|
this._refCounter.visit(obj.$ref);
|
|
|
|
if (visited && !forceCircular) {
|
2017-10-12 00:01:37 +03:00
|
|
|
// circular reference detected
|
|
|
|
return Object.assign({}, resolved, { 'x-circular-ref': true });
|
|
|
|
}
|
|
|
|
// deref again in case one more $ref is here
|
|
|
|
if (this.isRef(resolved)) {
|
|
|
|
const res = this.deref(resolved);
|
|
|
|
this.exitRef(resolved);
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
return resolved;
|
|
|
|
}
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Merge allOf contsraints.
|
|
|
|
* @param schema schema with allOF
|
|
|
|
* @param $ref pointer of the schema
|
|
|
|
* @param forceCircular whether to dereference children even if it is a cirular ref
|
|
|
|
*/
|
|
|
|
mergeAllOf(
|
|
|
|
schema: OpenAPISchema,
|
2017-12-07 19:38:49 +03:00
|
|
|
$ref?: string,
|
2017-10-12 00:01:37 +03:00
|
|
|
forceCircular: boolean = false,
|
|
|
|
): MergedOpenAPISchema {
|
|
|
|
if (schema.allOf === undefined) {
|
|
|
|
return schema;
|
|
|
|
}
|
|
|
|
|
|
|
|
let receiver: MergedOpenAPISchema = {
|
|
|
|
...schema,
|
|
|
|
allOf: undefined,
|
2017-12-07 19:38:49 +03:00
|
|
|
parentRefs: [],
|
2017-10-12 00:01:37 +03:00
|
|
|
};
|
|
|
|
|
2017-12-07 19:38:49 +03:00
|
|
|
const allOfSchemas = schema.allOf.map(subSchema => {
|
2017-11-23 16:27:50 +03:00
|
|
|
const resolved = this.deref(subSchema, forceCircular);
|
2017-12-07 19:38:49 +03:00
|
|
|
const subRef = subSchema.$ref || undefined;
|
2017-11-24 12:45:31 +03:00
|
|
|
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular);
|
2017-12-07 19:38:49 +03:00
|
|
|
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
|
2017-10-12 00:01:37 +03:00
|
|
|
return {
|
2017-11-23 16:27:50 +03:00
|
|
|
$ref: subRef,
|
2017-11-24 12:45:31 +03:00
|
|
|
schema: subMerged,
|
2017-10-12 00:01:37 +03:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
if (receiver.title === undefined && isNamedDefinition($ref)) {
|
|
|
|
receiver.title = JsonPointer.baseName($ref);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
|
|
|
|
if (
|
|
|
|
receiver.type !== subSchema.type &&
|
|
|
|
receiver.type !== undefined &&
|
|
|
|
subSchema.type !== undefined
|
|
|
|
) {
|
|
|
|
throw new Error(`Uncopatible types in allOf at "${$ref}"`);
|
|
|
|
}
|
|
|
|
|
2017-11-24 12:45:31 +03:00
|
|
|
if (subSchema.type !== undefined) {
|
|
|
|
receiver.type = subSchema.type;
|
|
|
|
}
|
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
if (subSchema.properties !== undefined) {
|
|
|
|
// TODO: merge properties contents
|
|
|
|
receiver.properties = {
|
|
|
|
...(receiver.properties || {}),
|
|
|
|
...subSchema.properties,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (subSchema.required !== undefined) {
|
|
|
|
receiver.required = (receiver.required || []).concat(subSchema.required);
|
|
|
|
}
|
|
|
|
|
2017-12-07 19:38:49 +03:00
|
|
|
if (subSchemaRef) {
|
|
|
|
receiver.parentRefs!.push(subSchemaRef);
|
|
|
|
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
|
2017-10-12 00:01:37 +03:00
|
|
|
receiver.title = JsonPointer.baseName(subSchemaRef);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// merge rest of constraints
|
|
|
|
// TODO: do more intelegent merge
|
|
|
|
receiver = { ...subSchema, ...receiver };
|
|
|
|
}
|
|
|
|
return receiver;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find all derived definitions among #/components/schemas from any of $refs
|
|
|
|
* returns map of definition pointer to definition name
|
|
|
|
* @param $refs array of references to find derived from
|
|
|
|
*/
|
|
|
|
findDerived($refs: string[]): Dict<string> {
|
|
|
|
const res: Dict<string> = {};
|
2017-11-14 18:46:50 +03:00
|
|
|
const schemas = (this.spec.components && this.spec.components.schemas) || {};
|
2017-10-12 00:01:37 +03:00
|
|
|
for (let defName in schemas) {
|
|
|
|
const def = this.deref(schemas[defName]);
|
|
|
|
if (
|
|
|
|
def.allOf !== undefined &&
|
|
|
|
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
|
|
|
|
) {
|
|
|
|
res['#/components/schemas/' + defName] = defName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
}
|