feat: merge refs oas 3.1 (#1640)

This commit is contained in:
Roman Hotsiy 2021-06-09 15:43:52 +03:00 committed by GitHub
parent 0a4d172333
commit f4ea368f78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 76 deletions

View File

@ -964,8 +964,7 @@ components:
properties: properties:
id: id:
description: Category ID description: Category ID
allOf: $ref: '#/components/schemas/Id'
- $ref: '#/components/schemas/Id'
name: name:
description: Category name description: Category name
type: string type: string
@ -1015,12 +1014,10 @@ components:
properties: properties:
id: id:
description: Order ID description: Order ID
allOf: $ref: '#/components/schemas/Id'
- $ref: '#/components/schemas/Id'
petId: petId:
description: Pet ID description: Pet ID
allOf: $ref: '#/components/schemas/Id'
- $ref: '#/components/schemas/Id'
quantity: quantity:
type: integer type: integer
format: int32 format: int32
@ -1065,12 +1062,10 @@ components:
description: "Find more info here" description: "Find more info here"
url: "https://example.com" url: "https://example.com"
description: Pet ID description: Pet ID
allOf: $ref: '#/components/schemas/Id'
- $ref: '#/components/schemas/Id'
category: category:
description: Categories this pet belongs to description: Categories this pet belongs to
allOf: $ref: '#/components/schemas/Category'
- $ref: '#/components/schemas/Category'
name: name:
description: The name given to a pet description: The name given to a pet
type: string type: string
@ -1087,8 +1082,7 @@ components:
type: string type: string
format: url format: url
friend: friend:
allOf: $ref: '#/components/schemas/Pet'
- $ref: '#/components/schemas/Pet'
tags: tags:
description: Tags attached to the pet description: Tags attached to the pet
type: array type: array
@ -1117,8 +1111,7 @@ components:
properties: properties:
id: id:
description: Tag ID description: Tag ID
allOf: $ref: '#/components/schemas/Id'
- $ref: '#/components/schemas/Id'
name: name:
description: Tag name description: Tag name
type: string type: string
@ -1133,6 +1126,7 @@ components:
pet: pet:
oneOf: oneOf:
- $ref: '#/components/schemas/Pet' - $ref: '#/components/schemas/Pet'
title: Pettie
- $ref: '#/components/schemas/Tag' - $ref: '#/components/schemas/Tag'
username: username:
description: User supplied username description: User supplied username
@ -1179,10 +1173,9 @@ components:
content: content:
application/json: application/json:
schema: schema:
allOf: description: My Pet
- description: My Pet
title: Pettie title: Pettie
- $ref: '#/components/schemas/Pet' $ref: '#/components/schemas/Pet'
application/xml: application/xml:
schema: schema:
type: 'object' type: 'object'

View File

@ -45,9 +45,9 @@ class RefCounter {
export class OpenAPIParser { export class OpenAPIParser {
specUrl?: string; specUrl?: string;
spec: OpenAPISpec; spec: OpenAPISpec;
mergeRefs: Set<string>;
private _refCounter: RefCounter = new RefCounter(); private _refCounter: RefCounter = new RefCounter();
private allowMergeRefs: boolean = false;
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
@ -58,8 +58,7 @@ export class OpenAPIParser {
this.preprocess(spec); this.preprocess(spec);
this.spec = spec; this.spec = spec;
this.allowMergeRefs = spec.openapi.startsWith('3.1');
this.mergeRefs = new Set();
const href = IS_BROWSER ? window.location.href : ''; const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') { if (typeof specUrl === 'string') {
@ -149,7 +148,7 @@ export class OpenAPIParser {
* @param obj object to dereference * @param obj object to dereference
* @param forceCircular whether to dereference even if it is circular ref * @param forceCircular whether to dereference even if it is circular ref
*/ */
deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false): T { deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false, mergeAsAllOf = false): T {
if (this.isRef(obj)) { if (this.isRef(obj)) {
const schemaName = getDefinitionName(obj.$ref); const schemaName = getDefinitionName(obj.$ref);
if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) { if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) {
@ -165,16 +164,36 @@ export class OpenAPIParser {
return Object.assign({}, resolved, { 'x-circular-ref': true }); return Object.assign({}, resolved, { 'x-circular-ref': true });
} }
// deref again in case one more $ref is here // deref again in case one more $ref is here
let result = resolved;
if (this.isRef(resolved)) { if (this.isRef(resolved)) {
const res = this.deref(resolved); result = this.deref(resolved, false, mergeAsAllOf);
this.exitRef(resolved); this.exitRef(resolved);
return res;
} }
return resolved; return this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : result;
} }
return obj; 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<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)!;
@ -225,7 +244,7 @@ export class OpenAPIParser {
return undefined; return undefined;
} }
const resolved = this.deref(subSchema, forceCircular); const resolved = this.deref(subSchema, forceCircular, true);
const subRef = subSchema.$ref || undefined; const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs); const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
receiver.parentRefs!.push(...(subMerged.parentRefs || [])); receiver.parentRefs!.push(...(subMerged.parentRefs || []));
@ -234,7 +253,7 @@ export class OpenAPIParser {
schema: subMerged, schema: subMerged,
}; };
}) })
.filter(child => child !== undefined) as Array<{ .filter((child) => child !== undefined) as Array<{
$ref: string | undefined; $ref: string | undefined;
schema: MergedOpenAPISchema; schema: MergedOpenAPISchema;
}>; }>;
@ -265,7 +284,7 @@ export class OpenAPIParser {
{ allOf: [receiver.properties[prop], subSchema.properties[prop]] }, { allOf: [receiver.properties[prop], subSchema.properties[prop]] },
$ref + '/properties/' + prop, $ref + '/properties/' + prop,
); );
receiver.properties[prop] = mergedProp receiver.properties[prop] = mergedProp;
this.exitParents(mergedProp); // every prop resolution should have separate recursive stack this.exitParents(mergedProp); // every prop resolution should have separate recursive stack
} }
} }
@ -313,7 +332,7 @@ 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.indexOf(obj.$ref) > -1)
) { ) {
res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName]; res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
} }
@ -339,7 +358,7 @@ export class OpenAPIParser {
const beforeAllOf = allOf.slice(0, i); const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1); const afterAllOf = allOf.slice(i + 1);
return { return {
oneOf: sub.oneOf.map(part => { oneOf: sub.oneOf.map((part) => {
const merged = this.mergeAllOf({ const merged = this.mergeAllOf({
allOf: [...beforeAllOf, part, ...afterAllOf], allOf: [...beforeAllOf, part, ...afterAllOf],
}); });

View File

@ -76,7 +76,7 @@ export class SchemaModel {
makeObservable(this); makeObservable(this);
this.pointer = schemaOrRef.$ref || pointer || ''; 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.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
this.init(parser, isChild); this.init(parser, isChild);
@ -193,7 +193,7 @@ export class SchemaModel {
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) { private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map((variant, idx) => { 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); const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx);

View File

@ -1864,16 +1864,10 @@ Object {
"content": Object { "content": Object {
"application/json": Object { "application/json": Object {
"schema": Object { "schema": Object {
"allOf": Array [ "$ref": "#/components/schemas/Pet",
Object {
"description": "My Pet", "description": "My Pet",
"title": "Pettie", "title": "Pettie",
}, },
Object {
"$ref": "#/components/schemas/Pet",
},
],
},
}, },
"application/xml": Object { "application/xml": Object {
"schema": Object { "schema": Object {
@ -1952,11 +1946,7 @@ Object {
"Category": Object { "Category": Object {
"properties": Object { "properties": Object {
"id": Object { "id": Object {
"allOf": Array [
Object {
"$ref": "#/components/schemas/Id", "$ref": "#/components/schemas/Id",
},
],
"description": "Category ID", "description": "Category ID",
}, },
"name": Object { "name": Object {
@ -2039,19 +2029,11 @@ Object {
"type": "boolean", "type": "boolean",
}, },
"id": Object { "id": Object {
"allOf": Array [
Object {
"$ref": "#/components/schemas/Id", "$ref": "#/components/schemas/Id",
},
],
"description": "Order ID", "description": "Order ID",
}, },
"petId": Object { "petId": Object {
"allOf": Array [
Object {
"$ref": "#/components/schemas/Id", "$ref": "#/components/schemas/Id",
},
],
"description": "Pet ID", "description": "Pet ID",
}, },
"quantity": Object { "quantity": Object {
@ -2096,26 +2078,14 @@ Object {
}, },
"properties": Object { "properties": Object {
"category": Object { "category": Object {
"allOf": Array [
Object {
"$ref": "#/components/schemas/Category", "$ref": "#/components/schemas/Category",
},
],
"description": "Categories this pet belongs to", "description": "Categories this pet belongs to",
}, },
"friend": Object { "friend": Object {
"allOf": Array [
Object {
"$ref": "#/components/schemas/Pet", "$ref": "#/components/schemas/Pet",
}, },
],
},
"id": Object { "id": Object {
"allOf": Array [
Object {
"$ref": "#/components/schemas/Id", "$ref": "#/components/schemas/Id",
},
],
"description": "Pet ID", "description": "Pet ID",
"externalDocs": Object { "externalDocs": Object {
"description": "Find more info here", "description": "Find more info here",
@ -2186,11 +2156,7 @@ Object {
"Tag": Object { "Tag": Object {
"properties": Object { "properties": Object {
"id": Object { "id": Object {
"allOf": Array [
Object {
"$ref": "#/components/schemas/Id", "$ref": "#/components/schemas/Id",
},
],
"description": "Tag ID", "description": "Tag ID",
}, },
"name": Object { "name": Object {
@ -2239,6 +2205,7 @@ Object {
"oneOf": Array [ "oneOf": Array [
Object { Object {
"$ref": "#/components/schemas/Pet", "$ref": "#/components/schemas/Pet",
"title": "Pettie",
}, },
Object { Object {
"$ref": "#/components/schemas/Tag", "$ref": "#/components/schemas/Tag",