mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-24 14:00: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,
|
||||
"nullable": false,
|
||||
"options": "<<<filtered>>>",
|
||||
"parentRefs": Array [],
|
||||
"pattern": undefined,
|
||||
"rawSchema": Object {
|
||||
"default": undefined,
|
||||
"parentRefs": Array [],
|
||||
"type": "number",
|
||||
"x-derefered": true,
|
||||
},
|
||||
"readOnly": false,
|
||||
"schema": Object {
|
||||
"default": undefined,
|
||||
"parentRefs": Array [],
|
||||
"type": "number",
|
||||
"x-derefered": true,
|
||||
},
|
||||
"title": "",
|
||||
"type": "number",
|
||||
|
@ -76,19 +81,20 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"isPrimitive": true,
|
||||
"nullable": false,
|
||||
"options": "<<<filtered>>>",
|
||||
"parentRefs": Array [],
|
||||
"pattern": undefined,
|
||||
"rawSchema": Object {
|
||||
"allOf": undefined,
|
||||
"default": undefined,
|
||||
"parentRefs": Array [],
|
||||
"type": "string",
|
||||
"x-derefered": true,
|
||||
},
|
||||
"readOnly": false,
|
||||
"schema": Object {
|
||||
"allOf": undefined,
|
||||
"default": undefined,
|
||||
"parentRefs": Array [],
|
||||
"type": "string",
|
||||
"x-derefered": true,
|
||||
},
|
||||
"title": "",
|
||||
"type": "string",
|
||||
|
|
|
@ -9,7 +9,7 @@ import { isNamedDefinition } from '../utils/openapi';
|
|||
import { buildComponentComment, COMPONENT_REGEXP } from './MarkdownRenderer';
|
||||
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
|
||||
|
@ -105,7 +105,7 @@ export class OpenAPIParser {
|
|||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
return res || {};
|
||||
return JSON.parse(JSON.stringify(res)) || {};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -166,6 +166,59 @@ export class OpenAPIParser {
|
|||
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 {
|
||||
if (this.isRef(obj)) {
|
||||
return this.byRef<T>(obj.$ref)!;
|
||||
|
@ -181,53 +234,50 @@ export class OpenAPIParser {
|
|||
*/
|
||||
mergeAllOf(
|
||||
schema: OpenAPISchema,
|
||||
$ref?: string,
|
||||
forceCircular: boolean = false,
|
||||
): MergedOpenAPISchema {
|
||||
if (schema.allOf === undefined) {
|
||||
): OpenAPISchema {
|
||||
if (schema.allOf === undefined || schema['x-circular-ref']) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
let receiver: MergedOpenAPISchema = {
|
||||
let receiver: OpenAPISchema = {
|
||||
...schema,
|
||||
allOf: undefined,
|
||||
parentRefs: [],
|
||||
};
|
||||
|
||||
const allOfSchemas = schema.allOf.map(subSchema => {
|
||||
const resolved = this.deref(subSchema, forceCircular);
|
||||
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) {
|
||||
for (const subSchemaRaw of schema.allOf) {
|
||||
const subSchema = this.mergeAllOf(subSchemaRaw);
|
||||
if (
|
||||
receiver.type !== subSchema.type &&
|
||||
receiver.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) {
|
||||
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) {
|
||||
receiver.properties = receiver.properties || {};
|
||||
for (const prop in subSchema.properties) {
|
||||
const mergedProp = this.mergeAllOf(subSchema.properties[prop]);
|
||||
if (!receiver.properties[prop]) {
|
||||
receiver.properties[prop] = subSchema.properties[prop];
|
||||
receiver.properties[prop] = mergedProp;
|
||||
} else {
|
||||
// merge inner properties
|
||||
receiver.properties[prop] = this.mergeAllOf(
|
||||
{ allOf: [receiver.properties[prop], subSchema.properties[prop]] },
|
||||
$ref + '/properties/' + prop,
|
||||
{ allOf: [receiver.properties[prop], mergedProp] },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -235,32 +285,20 @@ export class OpenAPIParser {
|
|||
|
||||
if (subSchema.items !== undefined) {
|
||||
receiver.items = receiver.items || {};
|
||||
// merge inner properties
|
||||
receiver.items = this.mergeAllOf(
|
||||
{ allOf: [receiver.items, subSchema.items] },
|
||||
$ref + '/items',
|
||||
);
|
||||
}
|
||||
|
||||
if (subSchema.required !== undefined) {
|
||||
receiver.required = (receiver.required || []).concat(subSchema.required);
|
||||
const mergedItems = this.mergeAllOf(subSchema.items);
|
||||
if (!receiver.items) {
|
||||
receiver.items = mergedItems;
|
||||
} else {
|
||||
// merge inner items
|
||||
receiver.items = this.mergeAllOf(
|
||||
{ allOf: [receiver.items, mergedItems] },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// merge rest of constraints
|
||||
// TODO: do more intelegent merge
|
||||
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;
|
||||
|
@ -278,10 +316,11 @@ export class OpenAPIParser {
|
|||
const def = this.deref(schemas[defName]);
|
||||
if (
|
||||
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;
|
||||
}
|
||||
this.exitRef(schemas[defName]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export class MediaTypeModel {
|
|||
for (const subSchema of this.schema.oneOf) {
|
||||
this.examples[subSchema.title] = {
|
||||
value: Sampler.sample(
|
||||
subSchema.rawSchema,
|
||||
parser.mergeAllOf(parser.derefSchema(subSchema.rawSchema || {$ref: ''})),
|
||||
{ skipReadOnly: this.isRequestType, skipWriteOnly: !this.isRequestType },
|
||||
parser.spec,
|
||||
),
|
||||
|
@ -54,7 +54,7 @@ export class MediaTypeModel {
|
|||
this.examples = {
|
||||
default: new ExampleModel(parser, {
|
||||
value: Sampler.sample(
|
||||
info.schema,
|
||||
parser.mergeAllOf(parser.derefSchema(info.schema || {$ref: ''})),
|
||||
{ skipReadOnly: this.isRequestType, skipWriteOnly: !this.isRequestType },
|
||||
parser.spec,
|
||||
),
|
||||
|
|
|
@ -6,7 +6,7 @@ import { OpenAPIParser } from '../OpenAPIParser';
|
|||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
import { FieldModel } from './Field';
|
||||
|
||||
import { MergedOpenAPISchema } from '../';
|
||||
import { DereferedOpenAPISchema } from '../';
|
||||
import {
|
||||
detectType,
|
||||
humanizeConstraints,
|
||||
|
@ -49,8 +49,9 @@ export class SchemaModel {
|
|||
discriminatorProp: string;
|
||||
@observable activeOneOf: number = 0;
|
||||
|
||||
rawSchema: OpenAPISchema;
|
||||
schema: MergedOpenAPISchema;
|
||||
rawSchema: DereferedOpenAPISchema;
|
||||
parentRefs: string[];
|
||||
schema: OpenAPISchema;
|
||||
|
||||
/**
|
||||
* @param isChild if schema discriminator Child
|
||||
|
@ -64,16 +65,10 @@ export class SchemaModel {
|
|||
isChild: boolean = false,
|
||||
) {
|
||||
this._$ref = schemaOrRef.$ref || $ref || '';
|
||||
this.rawSchema = parser.deref(schemaOrRef);
|
||||
this.schema = parser.mergeAllOf(this.rawSchema, this._$ref, isChild);
|
||||
this.rawSchema = parser.derefSchema(schemaOrRef);
|
||||
this.parentRefs = this.rawSchema.parentRefs || [];
|
||||
this.schema = parser.mergeAllOf(this.rawSchema);
|
||||
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(
|
||||
schema: OpenAPISchema & {
|
||||
parentRefs?: string[];
|
||||
},
|
||||
schema: DereferedOpenAPISchema,
|
||||
parser: OpenAPIParser,
|
||||
) {
|
||||
const discriminator = getDiscriminator(schema)!;
|
||||
this.discriminatorProp = discriminator.propertyName;
|
||||
const derived = parser.findDerived([...(schema.parentRefs || []), this._$ref]);
|
||||
const derived = parser.findDerived([...(this.parentRefs || []), this._$ref]);
|
||||
|
||||
if (schema.oneOf) {
|
||||
for (const variant of schema.oneOf) {
|
||||
|
|
|
@ -18,6 +18,7 @@ export class SecurityRequirementModel {
|
|||
this.schemes = Object.keys(requirement || {})
|
||||
.map(id => {
|
||||
const scheme = parser.deref(schemes[id]);
|
||||
parser.exitRef(schemes[id]);
|
||||
const scopes = requirement[id] || [];
|
||||
|
||||
if (!scheme) {
|
||||
|
|
|
@ -24,6 +24,7 @@ export class SecuritySchemeModel {
|
|||
|
||||
constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) {
|
||||
const info = parser.deref(scheme);
|
||||
parser.exitRef(scheme);
|
||||
this.id = id;
|
||||
this.sectionId = SECURITY_SCHEMES_SECTION + id;
|
||||
this.type = info.type;
|
||||
|
|
Loading…
Reference in New Issue
Block a user