redoc/src/services/OpenAPIParser.ts

318 lines
8.9 KiB
TypeScript
Raw Normal View History

2017-10-12 00:01:37 +03:00
import { resolve as urlResolve } from 'url';
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/';
2017-10-12 00:01:37 +03:00
import { JsonPointer } from '../utils/JsonPointer';
import { isNamedDefinition, SECURITY_DEFINITIONS_COMPONENT_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
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,
specUrl?: string,
private options: RedocNormalizedOptions = new RedocNormalizedOptions({}),
2017-11-21 17:33:22 +03:00
) {
this.validate(spec);
2017-11-21 17:33:22 +03:00
this.preprocess(spec);
2017-10-12 00:01:37 +03:00
this.spec = spec;
2017-10-12 00:01:37 +03:00
const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') {
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) {
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 || '';
if (!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME)) {
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) {
// do nothing
2017-10-12 00:01:37 +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') {
// check in dev mode
2018-01-22 21:30:53 +03:00
for (const 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>) {
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)!;
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;
}
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,
$ref?: string,
2017-10-12 00:01:37 +03:00
forceCircular: boolean = false,
): MergedOpenAPISchema {
schema = this.hoistOneOfs(schema);
2017-10-12 00:01:37 +03:00
if (schema.allOf === undefined) {
return schema;
}
let receiver: MergedOpenAPISchema = {
...schema,
allOf: undefined,
parentRefs: [],
2017-10-12 00:01:37 +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 };
}
const allOfSchemas = schema.allOf.map(subSchema => {
2017-11-23 16:27:50 +03:00
const resolved = this.deref(subSchema, forceCircular);
const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular);
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
2017-10-12 00:01:37 +03:00
return {
2017-11-23 16:27:50 +03:00
$ref: subRef,
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
) {
throw new Error(`Incompatible types in allOf at "${$ref}"`);
2017-10-12 00:01:37 +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
}
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);
}
// merge rest of constraints
// TODO: do more intelegent merge
receiver = { ...subSchema, ...receiver };
if (subSchemaRef) {
receiver.parentRefs!.push(subSchemaRef);
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
// this is not so correct behaviour. comented out for now
// ref: https://github.com/Rebilly/ReDoc/issues/601
// receiver.title = JsonPointer.baseName(subSchemaRef);
2017-10-12 00:01:37 +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)) {
receiver.title = JsonPointer.baseName($ref);
2017-10-12 00:01:37 +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> = {};
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)
) {
res['#/components/schemas/' + defName] = def['x-discriminator-value'] || defName;
2017-10-12 00:01:37 +03:00
}
}
return res;
}
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
}