Refactor circular-ref detection

This commit is contained in:
Jérémy Derussé 2018-05-30 14:48:01 +02:00
parent d38f2f2a5a
commit 4adfdab124
No known key found for this signature in database
GPG Key ID: 2083FA5758C473D2
6 changed files with 106 additions and 66 deletions

View File

@ -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",

View File

@ -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;
}

View File

@ -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,
),

View File

@ -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) {

View File

@ -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) {

View File

@ -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;