From f4ea368f78a693fd70d48b5e0e5ffce3560432f4 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Wed, 9 Jun 2021 15:43:52 +0300 Subject: [PATCH] feat: merge refs oas 3.1 (#1640) --- demo/openapi-3-1.yaml | 29 ++++------ src/services/OpenAPIParser.ts | 43 +++++++++++---- src/services/models/Schema.ts | 4 +- .../loadAndBundleSpec.test.ts.snap | 55 ++++--------------- 4 files changed, 55 insertions(+), 76 deletions(-) diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml index 6f6496aa..c905d4d8 100644 --- a/demo/openapi-3-1.yaml +++ b/demo/openapi-3-1.yaml @@ -964,8 +964,7 @@ components: properties: id: description: Category ID - allOf: - - $ref: '#/components/schemas/Id' + $ref: '#/components/schemas/Id' name: description: Category name type: string @@ -1015,12 +1014,10 @@ components: properties: id: description: Order ID - allOf: - - $ref: '#/components/schemas/Id' + $ref: '#/components/schemas/Id' petId: description: Pet ID - allOf: - - $ref: '#/components/schemas/Id' + $ref: '#/components/schemas/Id' quantity: type: integer format: int32 @@ -1065,12 +1062,10 @@ components: description: "Find more info here" url: "https://example.com" description: Pet ID - allOf: - - $ref: '#/components/schemas/Id' + $ref: '#/components/schemas/Id' category: description: Categories this pet belongs to - allOf: - - $ref: '#/components/schemas/Category' + $ref: '#/components/schemas/Category' name: description: The name given to a pet type: string @@ -1087,8 +1082,7 @@ components: type: string format: url friend: - allOf: - - $ref: '#/components/schemas/Pet' + $ref: '#/components/schemas/Pet' tags: description: Tags attached to the pet type: array @@ -1117,8 +1111,7 @@ components: properties: id: description: Tag ID - allOf: - - $ref: '#/components/schemas/Id' + $ref: '#/components/schemas/Id' name: description: Tag name type: string @@ -1133,6 +1126,7 @@ components: pet: oneOf: - $ref: '#/components/schemas/Pet' + title: Pettie - $ref: '#/components/schemas/Tag' username: description: User supplied username @@ -1179,10 +1173,9 @@ components: content: application/json: schema: - allOf: - - description: My Pet - title: Pettie - - $ref: '#/components/schemas/Pet' + description: My Pet + title: Pettie + $ref: '#/components/schemas/Pet' application/xml: schema: type: 'object' diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index b0d3a8f4..11a001aa 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -45,9 +45,9 @@ class RefCounter { export class OpenAPIParser { specUrl?: string; spec: OpenAPISpec; - mergeRefs: Set; private _refCounter: RefCounter = new RefCounter(); + private allowMergeRefs: boolean = false; constructor( spec: OpenAPISpec, @@ -58,8 +58,7 @@ export class OpenAPIParser { this.preprocess(spec); this.spec = spec; - - this.mergeRefs = new Set(); + this.allowMergeRefs = spec.openapi.startsWith('3.1'); const href = IS_BROWSER ? window.location.href : ''; if (typeof specUrl === 'string') { @@ -149,7 +148,7 @@ export class OpenAPIParser { * @param obj object to dereference * @param forceCircular whether to dereference even if it is circular ref */ - deref(obj: OpenAPIRef | T, forceCircular = false): T { + deref(obj: OpenAPIRef | T, forceCircular = false, mergeAsAllOf = false): T { if (this.isRef(obj)) { const schemaName = getDefinitionName(obj.$ref); if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) { @@ -165,16 +164,36 @@ export class OpenAPIParser { return Object.assign({}, resolved, { 'x-circular-ref': true }); } // deref again in case one more $ref is here + let result = resolved; if (this.isRef(resolved)) { - const res = this.deref(resolved); + result = this.deref(resolved, false, mergeAsAllOf); this.exitRef(resolved); - return res; } - return resolved; + return this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : result; } return obj; } + mergeRefs(ref, resolved, mergeAsAllOf: boolean) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $ref, ...rest } = ref; + const keys = Object.keys(rest); + if (keys.length === 0) { + return resolved; + } + if (mergeAsAllOf && keys.some((k) => k !== 'description' && k !== 'title' && k !== 'externalDocs')) { + return { + allOf: [resolved, rest], + }; + } else { + // small optimization + return { + ...resolved, + ...rest, + }; + } + } + shalowDeref(obj: OpenAPIRef | T): T { if (this.isRef(obj)) { return this.byRef(obj.$ref)!; @@ -225,7 +244,7 @@ export class OpenAPIParser { return undefined; } - const resolved = this.deref(subSchema, forceCircular); + const resolved = this.deref(subSchema, forceCircular, true); const subRef = subSchema.$ref || undefined; const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs); receiver.parentRefs!.push(...(subMerged.parentRefs || [])); @@ -234,7 +253,7 @@ export class OpenAPIParser { schema: subMerged, }; }) - .filter(child => child !== undefined) as Array<{ + .filter((child) => child !== undefined) as Array<{ $ref: string | undefined; schema: MergedOpenAPISchema; }>; @@ -265,7 +284,7 @@ export class OpenAPIParser { { allOf: [receiver.properties[prop], subSchema.properties[prop]] }, $ref + '/properties/' + prop, ); - receiver.properties[prop] = mergedProp + receiver.properties[prop] = mergedProp; this.exitParents(mergedProp); // every prop resolution should have separate recursive stack } } @@ -313,7 +332,7 @@ 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.indexOf(obj.$ref) > -1) ) { res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName]; } @@ -339,7 +358,7 @@ export class OpenAPIParser { const beforeAllOf = allOf.slice(0, i); const afterAllOf = allOf.slice(i + 1); return { - oneOf: sub.oneOf.map(part => { + oneOf: sub.oneOf.map((part) => { const merged = this.mergeAllOf({ allOf: [...beforeAllOf, part, ...afterAllOf], }); diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index f8971ac4..adb2de19 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -76,7 +76,7 @@ export class SchemaModel { makeObservable(this); this.pointer = schemaOrRef.$ref || pointer || ''; - this.rawSchema = parser.deref(schemaOrRef); + this.rawSchema = parser.deref(schemaOrRef, false, true); this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild); this.init(parser, isChild); @@ -193,7 +193,7 @@ export class SchemaModel { private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) { this.oneOf = oneOf!.map((variant, idx) => { - const derefVariant = parser.deref(variant); + const derefVariant = parser.deref(variant, false, true); const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx); diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index c8788037..1aa507f9 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -1864,15 +1864,9 @@ Object { "content": Object { "application/json": Object { "schema": Object { - "allOf": Array [ - Object { - "description": "My Pet", - "title": "Pettie", - }, - Object { - "$ref": "#/components/schemas/Pet", - }, - ], + "$ref": "#/components/schemas/Pet", + "description": "My Pet", + "title": "Pettie", }, }, "application/xml": Object { @@ -1952,11 +1946,7 @@ Object { "Category": Object { "properties": Object { "id": Object { - "allOf": Array [ - Object { - "$ref": "#/components/schemas/Id", - }, - ], + "$ref": "#/components/schemas/Id", "description": "Category ID", }, "name": Object { @@ -2039,19 +2029,11 @@ Object { "type": "boolean", }, "id": Object { - "allOf": Array [ - Object { - "$ref": "#/components/schemas/Id", - }, - ], + "$ref": "#/components/schemas/Id", "description": "Order ID", }, "petId": Object { - "allOf": Array [ - Object { - "$ref": "#/components/schemas/Id", - }, - ], + "$ref": "#/components/schemas/Id", "description": "Pet ID", }, "quantity": Object { @@ -2096,26 +2078,14 @@ Object { }, "properties": Object { "category": Object { - "allOf": Array [ - Object { - "$ref": "#/components/schemas/Category", - }, - ], + "$ref": "#/components/schemas/Category", "description": "Categories this pet belongs to", }, "friend": Object { - "allOf": Array [ - Object { - "$ref": "#/components/schemas/Pet", - }, - ], + "$ref": "#/components/schemas/Pet", }, "id": Object { - "allOf": Array [ - Object { - "$ref": "#/components/schemas/Id", - }, - ], + "$ref": "#/components/schemas/Id", "description": "Pet ID", "externalDocs": Object { "description": "Find more info here", @@ -2186,11 +2156,7 @@ Object { "Tag": Object { "properties": Object { "id": Object { - "allOf": Array [ - Object { - "$ref": "#/components/schemas/Id", - }, - ], + "$ref": "#/components/schemas/Id", "description": "Tag ID", }, "name": Object { @@ -2239,6 +2205,7 @@ Object { "oneOf": Array [ Object { "$ref": "#/components/schemas/Pet", + "title": "Pettie", }, Object { "$ref": "#/components/schemas/Tag",