mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-24 22:10:33 +03:00
Refactor circular-ref detection
This commit is contained in:
parent
d38f2f2a5a
commit
4adfdab124
|
@ -29,15 +29,20 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
||||||
"isPrimitive": true,
|
"isPrimitive": true,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"options": "<<<filtered>>>",
|
"options": "<<<filtered>>>",
|
||||||
|
"parentRefs": Array [],
|
||||||
"pattern": undefined,
|
"pattern": undefined,
|
||||||
"rawSchema": Object {
|
"rawSchema": Object {
|
||||||
"default": undefined,
|
"default": undefined,
|
||||||
|
"parentRefs": Array [],
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
"x-derefered": true,
|
||||||
},
|
},
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"schema": Object {
|
"schema": Object {
|
||||||
"default": undefined,
|
"default": undefined,
|
||||||
|
"parentRefs": Array [],
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
"x-derefered": true,
|
||||||
},
|
},
|
||||||
"title": "",
|
"title": "",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
@ -76,19 +81,20 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
||||||
"isPrimitive": true,
|
"isPrimitive": true,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"options": "<<<filtered>>>",
|
"options": "<<<filtered>>>",
|
||||||
|
"parentRefs": Array [],
|
||||||
"pattern": undefined,
|
"pattern": undefined,
|
||||||
"rawSchema": Object {
|
"rawSchema": Object {
|
||||||
"allOf": undefined,
|
|
||||||
"default": undefined,
|
"default": undefined,
|
||||||
"parentRefs": Array [],
|
"parentRefs": Array [],
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"x-derefered": true,
|
||||||
},
|
},
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"schema": Object {
|
"schema": Object {
|
||||||
"allOf": undefined,
|
|
||||||
"default": undefined,
|
"default": undefined,
|
||||||
"parentRefs": Array [],
|
"parentRefs": Array [],
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"x-derefered": true,
|
||||||
},
|
},
|
||||||
"title": "",
|
"title": "",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { isNamedDefinition } from '../utils/openapi';
|
||||||
import { buildComponentComment, COMPONENT_REGEXP } from './MarkdownRenderer';
|
import { buildComponentComment, COMPONENT_REGEXP } from './MarkdownRenderer';
|
||||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||||
|
|
||||||
export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
|
export type DereferedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class to keep track of visited references to avoid
|
* Helper class to keep track of visited references to avoid
|
||||||
|
@ -105,7 +105,7 @@ export class OpenAPIParser {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
return res || {};
|
return JSON.parse(JSON.stringify(res)) || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -166,6 +166,59 @@ export class OpenAPIParser {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
derefSchema(schema: OpenAPISchema): DereferedOpenAPISchema {
|
||||||
|
if (schema['x-derefered']) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiver: DereferedOpenAPISchema = {
|
||||||
|
...JSON.parse(JSON.stringify(this.deref<OpenAPISchema>(schema)!)),
|
||||||
|
parentRefs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isRef(schema)) {
|
||||||
|
receiver.parentRefs!.push(schema.$ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRef(schema) && receiver.title === undefined && isNamedDefinition(schema.$ref)) {
|
||||||
|
receiver.title = JsonPointer.baseName(schema.$ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiver['x-circular-ref']) {
|
||||||
|
this.exitRef(schema);
|
||||||
|
return receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const property of ['properties', 'anyOf', 'allOf', 'oneOf']) {
|
||||||
|
if (receiver[property] !== undefined) {
|
||||||
|
for (const prop in receiver[property]) {
|
||||||
|
const subSchema = this.derefSchema(receiver[property][prop]);
|
||||||
|
receiver.parentRefs!.push(...(subSchema.parentRefs || []));
|
||||||
|
|
||||||
|
receiver[property][prop] = subSchema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const property of ['items']) {
|
||||||
|
if (receiver[property] !== undefined) {
|
||||||
|
const subSchema = this.derefSchema(receiver[property]);
|
||||||
|
receiver.parentRefs!.push(...(subSchema.parentRefs || []));
|
||||||
|
|
||||||
|
receiver[property] = subSchema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exitRef(schema);
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
|
return Object.assign({}, receiver, { 'x-derefered': true });
|
||||||
|
}
|
||||||
|
|
||||||
shalowDeref<T extends object>(obj: OpenAPIRef | T): T {
|
shalowDeref<T extends object>(obj: OpenAPIRef | T): T {
|
||||||
if (this.isRef(obj)) {
|
if (this.isRef(obj)) {
|
||||||
return this.byRef<T>(obj.$ref)!;
|
return this.byRef<T>(obj.$ref)!;
|
||||||
|
@ -181,53 +234,50 @@ export class OpenAPIParser {
|
||||||
*/
|
*/
|
||||||
mergeAllOf(
|
mergeAllOf(
|
||||||
schema: OpenAPISchema,
|
schema: OpenAPISchema,
|
||||||
$ref?: string,
|
): OpenAPISchema {
|
||||||
forceCircular: boolean = false,
|
if (schema.allOf === undefined || schema['x-circular-ref']) {
|
||||||
): MergedOpenAPISchema {
|
|
||||||
if (schema.allOf === undefined) {
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
let receiver: MergedOpenAPISchema = {
|
let receiver: OpenAPISchema = {
|
||||||
...schema,
|
...schema,
|
||||||
allOf: undefined,
|
allOf: undefined,
|
||||||
parentRefs: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const allOfSchemas = schema.allOf.map(subSchema => {
|
for (const subSchemaRaw of schema.allOf) {
|
||||||
const resolved = this.deref(subSchema, forceCircular);
|
const subSchema = this.mergeAllOf(subSchemaRaw);
|
||||||
const subRef = subSchema.$ref || undefined;
|
|
||||||
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular);
|
|
||||||
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
|
|
||||||
return {
|
|
||||||
$ref: subRef,
|
|
||||||
schema: subMerged,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
|
|
||||||
if (
|
if (
|
||||||
receiver.type !== subSchema.type &&
|
receiver.type !== subSchema.type &&
|
||||||
receiver.type !== undefined &&
|
receiver.type !== undefined &&
|
||||||
subSchema.type !== undefined
|
subSchema.type !== undefined
|
||||||
) {
|
) {
|
||||||
throw new Error(`Incompatible types in allOf at "${$ref}"`);
|
throw new Error(`Incompatible types in allOf at "${schema.title}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subSchema.type !== undefined) {
|
if (subSchema.type !== undefined) {
|
||||||
receiver.type = subSchema.type;
|
receiver.type = subSchema.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (receiver.title === undefined) {
|
||||||
|
receiver.title = subSchema.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!subSchema['x-circular-ref']) {
|
||||||
|
receiver = { ...subSchema, ...receiver };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
receiver.required = [...(receiver.required || []), ...(subSchema.required || [])];
|
||||||
if (subSchema.properties !== undefined) {
|
if (subSchema.properties !== undefined) {
|
||||||
receiver.properties = receiver.properties || {};
|
receiver.properties = receiver.properties || {};
|
||||||
for (const prop in subSchema.properties) {
|
for (const prop in subSchema.properties) {
|
||||||
|
const mergedProp = this.mergeAllOf(subSchema.properties[prop]);
|
||||||
if (!receiver.properties[prop]) {
|
if (!receiver.properties[prop]) {
|
||||||
receiver.properties[prop] = subSchema.properties[prop];
|
receiver.properties[prop] = mergedProp;
|
||||||
} else {
|
} else {
|
||||||
// merge inner properties
|
// merge inner properties
|
||||||
receiver.properties[prop] = this.mergeAllOf(
|
receiver.properties[prop] = this.mergeAllOf(
|
||||||
{ allOf: [receiver.properties[prop], subSchema.properties[prop]] },
|
{ allOf: [receiver.properties[prop], mergedProp] },
|
||||||
$ref + '/properties/' + prop,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,32 +285,20 @@ export class OpenAPIParser {
|
||||||
|
|
||||||
if (subSchema.items !== undefined) {
|
if (subSchema.items !== undefined) {
|
||||||
receiver.items = receiver.items || {};
|
receiver.items = receiver.items || {};
|
||||||
// merge inner properties
|
const mergedItems = this.mergeAllOf(subSchema.items);
|
||||||
receiver.items = this.mergeAllOf(
|
if (!receiver.items) {
|
||||||
{ allOf: [receiver.items, subSchema.items] },
|
receiver.items = mergedItems;
|
||||||
$ref + '/items',
|
} else {
|
||||||
);
|
// merge inner items
|
||||||
}
|
receiver.items = this.mergeAllOf(
|
||||||
|
{ allOf: [receiver.items, mergedItems] },
|
||||||
if (subSchema.required !== undefined) {
|
);
|
||||||
receiver.required = (receiver.required || []).concat(subSchema.required);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge rest of constraints
|
// merge rest of constraints
|
||||||
// TODO: do more intelegent merge
|
// TODO: do more intelegent merge
|
||||||
receiver = { ...subSchema, ...receiver };
|
receiver = { ...subSchema, ...receiver };
|
||||||
|
|
||||||
if (subSchemaRef) {
|
|
||||||
receiver.parentRefs!.push(subSchemaRef);
|
|
||||||
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
|
|
||||||
receiver.title = JsonPointer.baseName(subSchemaRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// name of definition or title on top level
|
|
||||||
if (schema.title === undefined && isNamedDefinition($ref)) {
|
|
||||||
receiver.title = JsonPointer.baseName($ref);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return receiver;
|
return receiver;
|
||||||
|
@ -278,10 +316,11 @@ export class OpenAPIParser {
|
||||||
const def = this.deref(schemas[defName]);
|
const def = this.deref(schemas[defName]);
|
||||||
if (
|
if (
|
||||||
def.allOf !== undefined &&
|
def.allOf !== undefined &&
|
||||||
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
|
def.allOf.find(obj => obj.$ref !== undefined && $refs.includes(obj.$ref))
|
||||||
) {
|
) {
|
||||||
res['#/components/schemas/' + defName] = defName;
|
res['#/components/schemas/' + defName] = defName;
|
||||||
}
|
}
|
||||||
|
this.exitRef(schemas[defName]);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class MediaTypeModel {
|
||||||
for (const subSchema of this.schema.oneOf) {
|
for (const subSchema of this.schema.oneOf) {
|
||||||
this.examples[subSchema.title] = {
|
this.examples[subSchema.title] = {
|
||||||
value: Sampler.sample(
|
value: Sampler.sample(
|
||||||
subSchema.rawSchema,
|
parser.mergeAllOf(parser.derefSchema(subSchema.rawSchema || {$ref: ''})),
|
||||||
{ skipReadOnly: this.isRequestType, skipWriteOnly: !this.isRequestType },
|
{ skipReadOnly: this.isRequestType, skipWriteOnly: !this.isRequestType },
|
||||||
parser.spec,
|
parser.spec,
|
||||||
),
|
),
|
||||||
|
@ -54,7 +54,7 @@ export class MediaTypeModel {
|
||||||
this.examples = {
|
this.examples = {
|
||||||
default: new ExampleModel(parser, {
|
default: new ExampleModel(parser, {
|
||||||
value: Sampler.sample(
|
value: Sampler.sample(
|
||||||
info.schema,
|
parser.mergeAllOf(parser.derefSchema(info.schema || {$ref: ''})),
|
||||||
{ skipReadOnly: this.isRequestType, skipWriteOnly: !this.isRequestType },
|
{ skipReadOnly: this.isRequestType, skipWriteOnly: !this.isRequestType },
|
||||||
parser.spec,
|
parser.spec,
|
||||||
),
|
),
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { OpenAPIParser } from '../OpenAPIParser';
|
||||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||||
import { FieldModel } from './Field';
|
import { FieldModel } from './Field';
|
||||||
|
|
||||||
import { MergedOpenAPISchema } from '../';
|
import { DereferedOpenAPISchema } from '../';
|
||||||
import {
|
import {
|
||||||
detectType,
|
detectType,
|
||||||
humanizeConstraints,
|
humanizeConstraints,
|
||||||
|
@ -49,8 +49,9 @@ export class SchemaModel {
|
||||||
discriminatorProp: string;
|
discriminatorProp: string;
|
||||||
@observable activeOneOf: number = 0;
|
@observable activeOneOf: number = 0;
|
||||||
|
|
||||||
rawSchema: OpenAPISchema;
|
rawSchema: DereferedOpenAPISchema;
|
||||||
schema: MergedOpenAPISchema;
|
parentRefs: string[];
|
||||||
|
schema: OpenAPISchema;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param isChild if schema discriminator Child
|
* @param isChild if schema discriminator Child
|
||||||
|
@ -64,16 +65,10 @@ export class SchemaModel {
|
||||||
isChild: boolean = false,
|
isChild: boolean = false,
|
||||||
) {
|
) {
|
||||||
this._$ref = schemaOrRef.$ref || $ref || '';
|
this._$ref = schemaOrRef.$ref || $ref || '';
|
||||||
this.rawSchema = parser.deref(schemaOrRef);
|
this.rawSchema = parser.derefSchema(schemaOrRef);
|
||||||
this.schema = parser.mergeAllOf(this.rawSchema, this._$ref, isChild);
|
this.parentRefs = this.rawSchema.parentRefs || [];
|
||||||
|
this.schema = parser.mergeAllOf(this.rawSchema);
|
||||||
this.init(parser, isChild);
|
this.init(parser, isChild);
|
||||||
|
|
||||||
parser.exitRef(schemaOrRef);
|
|
||||||
|
|
||||||
for (const parent$ref of this.schema.parentRefs || []) {
|
|
||||||
// exit all the refs visited during allOf traverse
|
|
||||||
parser.exitRef({ $ref: parent$ref });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -166,14 +161,12 @@ export class SchemaModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initDiscriminator(
|
private initDiscriminator(
|
||||||
schema: OpenAPISchema & {
|
schema: DereferedOpenAPISchema,
|
||||||
parentRefs?: string[];
|
|
||||||
},
|
|
||||||
parser: OpenAPIParser,
|
parser: OpenAPIParser,
|
||||||
) {
|
) {
|
||||||
const discriminator = getDiscriminator(schema)!;
|
const discriminator = getDiscriminator(schema)!;
|
||||||
this.discriminatorProp = discriminator.propertyName;
|
this.discriminatorProp = discriminator.propertyName;
|
||||||
const derived = parser.findDerived([...(schema.parentRefs || []), this._$ref]);
|
const derived = parser.findDerived([...(this.parentRefs || []), this._$ref]);
|
||||||
|
|
||||||
if (schema.oneOf) {
|
if (schema.oneOf) {
|
||||||
for (const variant of schema.oneOf) {
|
for (const variant of schema.oneOf) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ export class SecurityRequirementModel {
|
||||||
this.schemes = Object.keys(requirement || {})
|
this.schemes = Object.keys(requirement || {})
|
||||||
.map(id => {
|
.map(id => {
|
||||||
const scheme = parser.deref(schemes[id]);
|
const scheme = parser.deref(schemes[id]);
|
||||||
|
parser.exitRef(schemes[id]);
|
||||||
const scopes = requirement[id] || [];
|
const scopes = requirement[id] || [];
|
||||||
|
|
||||||
if (!scheme) {
|
if (!scheme) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ export class SecuritySchemeModel {
|
||||||
|
|
||||||
constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) {
|
constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) {
|
||||||
const info = parser.deref(scheme);
|
const info = parser.deref(scheme);
|
||||||
|
parser.exitRef(scheme);
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.sectionId = SECURITY_SCHEMES_SECTION + id;
|
this.sectionId = SECURITY_SCHEMES_SECTION + id;
|
||||||
this.type = info.type;
|
this.type = info.type;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user