2017-10-12 00:01:37 +03:00
|
|
|
import { resolve as urlResolve } from 'url';
|
|
|
|
|
|
|
|
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
|
|
|
|
|
2018-03-26 18:14:38 +03:00
|
|
|
import { appendToMdHeading, IS_BROWSER } from '../utils/';
|
2017-10-12 00:01:37 +03:00
|
|
|
import { JsonPointer } from '../utils/JsonPointer';
|
|
|
|
import { isNamedDefinition } from '../utils/openapi';
|
2018-08-16 12:37:39 +03:00
|
|
|
import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer';
|
2017-11-21 17:33:22 +03:00
|
|
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
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 {
|
2018-01-22 21:30:53 +03:00
|
|
|
_counter = {};
|
2017-10-12 00:01:37 +03:00
|
|
|
|
|
|
|
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 {
|
2018-07-26 17:34:44 +03:00
|
|
|
specUrl?: string;
|
|
|
|
spec: OpenAPISpec;
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2018-01-22 21:30:53 +03:00
|
|
|
private _refCounter: RefCounter = new RefCounter();
|
|
|
|
|
2017-11-21 17:33:22 +03:00
|
|
|
constructor(
|
|
|
|
spec: OpenAPISpec,
|
2018-05-14 11:37:19 +03:00
|
|
|
specUrl?: string,
|
|
|
|
private options: RedocNormalizedOptions = new RedocNormalizedOptions({}),
|
2017-11-21 17:33:22 +03:00
|
|
|
) {
|
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
|
|
|
|
2018-03-26 18:14:38 +03:00
|
|
|
const href = IS_BROWSER ? window.location.href : '';
|
2017-11-14 18:46:50 +03:00
|
|
|
if (typeof specUrl === 'string') {
|
2018-01-09 20:00:17 +03:00
|
|
|
this.specUrl = urlResolve(href, specUrl);
|
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 || '';
|
2018-08-16 12:37:39 +03:00
|
|
|
if (!MarkdownRenderer.containsComponent(description, 'security-definitions')) {
|
2017-11-21 17:33:22 +03:00
|
|
|
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;
|
2018-01-22 21:30:53 +03:00
|
|
|
if (!this.spec) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
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
|
|
|
}
|
2018-03-14 09:05:57 +03:00
|
|
|
return res || {};
|
2017-10-12 00:01:37 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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() {
|
2018-03-17 18:58:28 +03:00
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
2017-11-24 12:45:31 +03:00
|
|
|
// check in dev mode
|
2018-01-22 21:30:53 +03:00
|
|
|
for (const k in this._refCounter._counter) {
|
2017-11-24 12:45:31 +03:00
|
|
|
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>) {
|
2018-01-22 21:30:53 +03:00
|
|
|
if (!this.isRef(ref)) {
|
|
|
|
return;
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
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
|
2018-01-22 21:30:53 +03:00
|
|
|
// tslint:disable-next-line
|
2017-10-12 00:01:37 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-05-14 11:37:19 +03:00
|
|
|
shalowDeref<T extends object>(obj: OpenAPIRef | T): T {
|
|
|
|
if (this.isRef(obj)) {
|
|
|
|
return this.byRef<T>(obj.$ref)!;
|
|
|
|
}
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
/**
|
|
|
|
* 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 {
|
2018-07-18 13:03:42 +03:00
|
|
|
schema = this.hoistOneOfs(schema);
|
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
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
|
|
|
};
|
|
|
|
|
2018-07-18 11:42:02 +03:00
|
|
|
// avoid mutating inner objects
|
|
|
|
if (receiver.properties !== undefined && typeof receiver.properties === 'object') {
|
|
|
|
receiver.properties = { ...receiver.properties };
|
|
|
|
}
|
|
|
|
if (receiver.items !== undefined && typeof receiver.items === 'object') {
|
|
|
|
receiver.items = { ...receiver.items };
|
|
|
|
}
|
|
|
|
|
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
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2018-01-22 21:30:53 +03:00
|
|
|
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
|
2017-10-12 00:01:37 +03:00
|
|
|
if (
|
|
|
|
receiver.type !== subSchema.type &&
|
|
|
|
receiver.type !== undefined &&
|
|
|
|
subSchema.type !== undefined
|
|
|
|
) {
|
2018-04-17 08:51:08 +03:00
|
|
|
throw new Error(`Incompatible types in allOf at "${$ref}"`);
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
|
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) {
|
2018-03-05 14:47:04 +03:00
|
|
|
receiver.properties = receiver.properties || {};
|
2018-03-05 15:37:42 +03:00
|
|
|
for (const prop in subSchema.properties) {
|
2018-03-05 14:47:04 +03:00
|
|
|
if (!receiver.properties[prop]) {
|
|
|
|
receiver.properties[prop] = subSchema.properties[prop];
|
|
|
|
} else {
|
|
|
|
// merge inner properties
|
|
|
|
receiver.properties[prop] = this.mergeAllOf(
|
|
|
|
{ allOf: [receiver.properties[prop], subSchema.properties[prop]] },
|
|
|
|
$ref + '/properties/' + prop,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
|
2018-05-31 12:59:55 +03:00
|
|
|
if (subSchema.items !== undefined) {
|
|
|
|
receiver.items = receiver.items || {};
|
|
|
|
// merge inner properties
|
|
|
|
receiver.items = this.mergeAllOf(
|
|
|
|
{ allOf: [receiver.items, subSchema.items] },
|
|
|
|
$ref + '/items',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
if (subSchema.required !== undefined) {
|
|
|
|
receiver.required = (receiver.required || []).concat(subSchema.required);
|
|
|
|
}
|
|
|
|
|
2018-01-10 14:02:32 +03:00
|
|
|
// merge rest of constraints
|
|
|
|
// TODO: do more intelegent merge
|
|
|
|
receiver = { ...subSchema, ...receiver };
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2018-01-10 14:02:32 +03:00
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
|
2018-01-16 18:10:42 +03:00
|
|
|
// name of definition or title on top level
|
|
|
|
if (schema.title === undefined && isNamedDefinition($ref)) {
|
2018-01-10 14:02:32 +03:00
|
|
|
receiver.title = JsonPointer.baseName($ref);
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
2018-01-10 14:02:32 +03:00
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
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) || {};
|
2018-01-22 21:30:53 +03:00
|
|
|
for (const defName in schemas) {
|
2017-10-12 00:01:37 +03:00
|
|
|
const def = this.deref(schemas[defName]);
|
|
|
|
if (
|
|
|
|
def.allOf !== undefined &&
|
|
|
|
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
|
|
|
|
) {
|
2018-06-20 19:08:19 +03:00
|
|
|
res['#/components/schemas/' + defName] = def['x-discriminator-value'] || defName;
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
2018-07-18 13:03:42 +03:00
|
|
|
|
|
|
|
private hoistOneOfs(schema: OpenAPISchema) {
|
|
|
|
if (schema.allOf === undefined) {
|
|
|
|
return schema;
|
|
|
|
}
|
|
|
|
|
|
|
|
const allOf = schema.allOf;
|
|
|
|
for (let i = 0; i < allOf.length; i++) {
|
|
|
|
const sub = allOf[i];
|
|
|
|
if (Array.isArray(sub.oneOf)) {
|
|
|
|
const beforeAllOf = allOf.slice(0, i);
|
|
|
|
const afterAllOf = allOf.slice(i + 1);
|
|
|
|
return {
|
|
|
|
oneOf: sub.oneOf.map(part => {
|
|
|
|
return this.mergeAllOf({
|
|
|
|
allOf: [...beforeAllOf, part, ...afterAllOf],
|
|
|
|
});
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return schema;
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|