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';
|
2019-09-30 11:20:55 +03:00
|
|
|
import {
|
|
|
|
isNamedDefinition,
|
|
|
|
SECURITY_DEFINITIONS_COMPONENT_NAME,
|
|
|
|
SECURITY_DEFINITIONS_JSX_NAME,
|
|
|
|
} 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;
|
2019-09-30 12:55:47 +03:00
|
|
|
mergeRefs: Set<string>;
|
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
|
|
|
|
2019-09-30 12:55:47 +03:00
|
|
|
this.mergeRefs = new Set();
|
|
|
|
|
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 || '';
|
2019-09-30 11:20:55 +03:00
|
|
|
if (
|
|
|
|
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME) &&
|
|
|
|
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_JSX_NAME)
|
|
|
|
) {
|
2018-08-17 14:17:16 +03:00
|
|
|
const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME);
|
2017-11-21 17:33:22 +03:00
|
|
|
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
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2019-12-10 09:13:37 +03:00
|
|
|
* checks if the object is OpenAPI reference (contains $ref property)
|
2017-10-12 00:01:37 +03:00
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-12-10 09:13:37 +03:00
|
|
|
* resets visited endpoints. should be run after
|
2017-10-12 00:01:37 +03:00
|
|
|
*/
|
|
|
|
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
|
2019-12-10 09:13:37 +03:00
|
|
|
* @param forceCircular whether to dereference even if it is circular ref
|
2017-10-12 00:01:37 +03:00
|
|
|
*/
|
2020-03-17 13:01:32 +03:00
|
|
|
deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false): T {
|
2017-10-12 00:01:37 +03:00
|
|
|
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
|
|
|
/**
|
2019-12-10 09:13:37 +03:00
|
|
|
* Merge allOf constraints.
|
2017-10-12 00:01:37 +03:00
|
|
|
* @param schema schema with allOF
|
|
|
|
* @param $ref pointer of the schema
|
2019-12-10 09:13:37 +03:00
|
|
|
* @param forceCircular whether to dereference children even if it is a circular ref
|
2017-10-12 00:01:37 +03:00
|
|
|
*/
|
|
|
|
mergeAllOf(
|
|
|
|
schema: OpenAPISchema,
|
2017-12-07 19:38:49 +03:00
|
|
|
$ref?: string,
|
2017-10-12 00:01:37 +03:00
|
|
|
forceCircular: boolean = false,
|
2019-09-30 12:55:47 +03:00
|
|
|
used$Refs = new Set<string>(),
|
2017-10-12 00:01:37 +03:00
|
|
|
): MergedOpenAPISchema {
|
2019-09-30 12:55:47 +03:00
|
|
|
if ($ref) {
|
|
|
|
used$Refs.add($ref);
|
|
|
|
}
|
|
|
|
|
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: [],
|
2019-05-12 21:38:00 +03:00
|
|
|
title: schema.title || (isNamedDefinition($ref) ? JsonPointer.baseName($ref) : undefined),
|
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 };
|
|
|
|
}
|
|
|
|
|
2019-09-30 12:55:47 +03:00
|
|
|
const allOfSchemas = schema.allOf
|
|
|
|
.map(subSchema => {
|
|
|
|
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const resolved = this.deref(subSchema, forceCircular);
|
|
|
|
const subRef = subSchema.$ref || undefined;
|
|
|
|
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
|
|
|
|
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
|
|
|
|
return {
|
|
|
|
$ref: subRef,
|
|
|
|
schema: subMerged,
|
|
|
|
};
|
|
|
|
})
|
|
|
|
.filter(child => child !== undefined) as Array<{
|
|
|
|
$ref: string | undefined;
|
|
|
|
schema: MergedOpenAPISchema;
|
|
|
|
}>;
|
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
|
|
|
|
) {
|
2020-05-10 21:59:48 +03:00
|
|
|
console.warn(
|
|
|
|
`Incompatible types in allOf at "${$ref}": "${receiver.type}" and "${subSchema.type}"`,
|
|
|
|
);
|
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
|
2019-12-10 09:13:37 +03:00
|
|
|
// TODO: do more intelligent merge
|
2018-01-10 14:02:32 +03:00
|
|
|
receiver = { ...subSchema, ...receiver };
|
|
|
|
|
2017-12-07 19:38:49 +03:00
|
|
|
if (subSchemaRef) {
|
|
|
|
receiver.parentRefs!.push(subSchemaRef);
|
|
|
|
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
|
2019-12-10 09:13:37 +03:00
|
|
|
// this is not so correct behaviour. commented out for now
|
2019-06-04 15:47:22 +03:00
|
|
|
// ref: https://github.com/Redocly/redoc/issues/601
|
2018-08-22 12:27:13 +03:00
|
|
|
// receiver.title = JsonPointer.baseName(subSchemaRef);
|
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
|
|
|
|
*/
|
2020-01-14 23:53:45 +03:00
|
|
|
findDerived($refs: string[]): Dict<string[] | string> {
|
2019-12-12 16:21:41 +03:00
|
|
|
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)
|
|
|
|
) {
|
2019-12-12 16:21:41 +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
|
|
|
|
2018-11-27 13:21:28 +03:00
|
|
|
exitParents(shema: MergedOpenAPISchema) {
|
|
|
|
for (const parent$ref of shema.parentRefs || []) {
|
|
|
|
this.exitRef({ $ref: parent$ref });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 => {
|
2018-11-27 13:21:28 +03:00
|
|
|
const merged = this.mergeAllOf({
|
2018-07-18 13:03:42 +03:00
|
|
|
allOf: [...beforeAllOf, part, ...afterAllOf],
|
|
|
|
});
|
2018-11-27 13:21:28 +03:00
|
|
|
|
|
|
|
// each oneOf should be independent so exiting all the parent refs
|
|
|
|
// otherwise it will cause false-positive recursive detection
|
|
|
|
this.exitParents(merged);
|
|
|
|
return merged;
|
2018-07-18 13:03:42 +03:00
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return schema;
|
|
|
|
}
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|