diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index a8cbccbe..e5d33097 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -29,15 +29,20 @@ exports[`Components SchemaView discriminator should correctly render discriminat "isPrimitive": true, "nullable": false, "options": "<<>>", + "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": "<<>>", + "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", diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index 8982bf3d..a872baa4 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -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(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(obj: OpenAPIRef | T): T { if (this.isRef(obj)) { return this.byRef(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; } diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts index 1465995b..c64b2946 100644 --- a/src/services/models/MediaType.ts +++ b/src/services/models/MediaType.ts @@ -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, ), diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index fda95339..be3d63c1 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -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) { diff --git a/src/services/models/SecurityRequirement.ts b/src/services/models/SecurityRequirement.ts index 62fe2ca4..14d88b50 100644 --- a/src/services/models/SecurityRequirement.ts +++ b/src/services/models/SecurityRequirement.ts @@ -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) { diff --git a/src/services/models/SecuritySchemes.ts b/src/services/models/SecuritySchemes.ts index 7b8452be..a556509b 100644 --- a/src/services/models/SecuritySchemes.ts +++ b/src/services/models/SecuritySchemes.ts @@ -24,6 +24,7 @@ export class SecuritySchemeModel { constructor(parser: OpenAPIParser, id: string, scheme: Referenced) { const info = parser.deref(scheme); + parser.exitRef(scheme); this.id = id; this.sectionId = SECURITY_SCHEMES_SECTION + id; this.type = info.type;