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

View File

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

View File

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

View File

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

View File

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

View File

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