From b5d6b50d352f9435e3af49aca1e048f6dab3c960 Mon Sep 17 00:00:00 2001 From: Andrey Klimenko Date: Thu, 14 Mar 2024 09:42:43 +0100 Subject: [PATCH] allof support --- package.json | 1 + src/generators/ModelsGenerator.ts | 71 +++++++++++--- .../models-generator/InterfacesGenerator.ts | 43 ++++++-- src/generators/utils/is-defined.ts | 3 + src/models/InterfaceModel.ts | 1 + src/models/ObjectModel.ts | 3 + src/services/ModelMappingService.ts | 97 ++++++++++++++++++- src/swagger/OpenAPIService.ts | 5 +- src/swagger/OpenAPITypesGuard.ts | 6 ++ src/swagger/v3/schemas/one-of-schema.ts | 6 ++ 10 files changed, 207 insertions(+), 29 deletions(-) create mode 100644 src/generators/utils/is-defined.ts create mode 100644 src/swagger/v3/schemas/one-of-schema.ts diff --git a/package.json b/package.json index 4349c03..7b164e5 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "g:withRequestOptions": "node ./bin/index.js g --all --file=./swagger.json --output=./.output/withRequestOptions --withRequestOptions", "g:alias": "node ./bin/index.js g --file=./swagger.json --aliasName alias --output=./.output/selected", "e2e": "npm run g && npm run g:withRequestOptions && ts-node ./e2e/e2e.ts", + "g:b": "npm run build && npm run g", "test": "jest", "test:w": "jest --watch", "coverage": "jest --coverage", diff --git a/src/generators/ModelsGenerator.ts b/src/generators/ModelsGenerator.ts index d237733..2de8ddc 100644 --- a/src/generators/ModelsGenerator.ts +++ b/src/generators/ModelsGenerator.ts @@ -1,5 +1,6 @@ import { ClassDeclarationStructure, + CodeBlockWriter, ConstructorDeclarationStructure, ImportDeclarationStructure, OptionalKind, @@ -125,7 +126,7 @@ export class ModelsGenerator { kind: StructureKind.Class, isExported: true, name: z.name, - properties: this.getObjectProperties(z), + properties: this.getObjectProperties(z, objects), methods: [ { scope: Scope.Public, @@ -138,6 +139,9 @@ export class ModelsGenerator { z.properties.forEach((p) => x.withIndentationLevel(3, () => x.writeLine(`${p.name}: ${this.getToDtoPropertyInitializer(p)},`)) ); + this.printCombinedProprs(z, x, objects, (p) => + x.withIndentationLevel(3, () => x.writeLine(`${p.name}: ${this.getToDtoPropertyInitializer(p)},`)) + ); x.writeLine('};'); } }, @@ -152,6 +156,9 @@ export class ModelsGenerator { z.properties.forEach((p) => x.withIndentationLevel(2, () => x.writeLine(`model.${p.name} = ${this.getFromDtoPropertyInitializer(p)};`)) ); + this.printCombinedProprs(z, x, objects, (p) => + x.withIndentationLevel(2, () => x.writeLine(`model.${p.name} = ${this.getFromDtoPropertyInitializer(p)};`)) + ); x.writeLine('return model;'); } } @@ -159,25 +166,39 @@ export class ModelsGenerator { })); } - private getObjectProperties(objectModel: IObjectModel): PropertyDeclarationStructure[] { + private getObjectProperties(objectModel: IObjectModel, objects: IObjectModel[]): PropertyDeclarationStructure[] { return [ - ...objectModel.properties.map( - (objectProperty): PropertyDeclarationStructure => ({ - kind: StructureKind.Property, - scope: Scope.Public, - name: objectProperty.name, - type: new TypeSerializer({ - type: { name: objectProperty.type }, - isNullable: objectProperty.isNullable, - isCollection: objectProperty.isCollection - }).toString(), - initializer: objectProperty.isCollection ? ARRAY_STRING : UNDEFINED_STRING - }) - ), + ...this.getObjectCombinedProperties(objectModel, objects), + ...objectModel.properties.map((objectProperty) => this.getDeclarationStructure(objectProperty)), this.getGuardProperty(objectModel.name) ]; } + private getObjectCombinedProperties(objectModel: IObjectModel, objects: IObjectModel[]): PropertyDeclarationStructure[] { + return objectModel.combineTypes.reduce((acc, item) => { + const model = objects.find((x) => x.name === item); + const props = (model?.properties ?? []).map((objectProperty) => this.getDeclarationStructure(objectProperty)); + if (model) { + props.push(...this.getObjectCombinedProperties(model, objects)); + } + return [...acc, ...props]; + }, [] as PropertyDeclarationStructure[]); + } + + private getDeclarationStructure(objectProperty: IObjectPropertyModel): PropertyDeclarationStructure { + return { + kind: StructureKind.Property, + scope: Scope.Public, + name: objectProperty.name, + type: new TypeSerializer({ + type: { name: objectProperty.type }, + isNullable: objectProperty.isNullable, + isCollection: objectProperty.isCollection + }).toString(), + initializer: objectProperty.isCollection ? ARRAY_STRING : UNDEFINED_STRING + }; + } + private getGuardProperty(name: string): PropertyDeclarationStructure { return { kind: StructureKind.Property, @@ -271,4 +292,24 @@ export class ModelsGenerator { return dtoProperty; } } + + private printCombinedProprs( + model: IObjectModel | undefined, + writer: CodeBlockWriter, + objectsCollection: IObjectModel[], + printFunction: (p: IObjectPropertyModel) => void + ): void { + if (!model) { + return; + } + model.combineTypes.forEach((y) => { + (objectsCollection.find((x) => x.name === y)?.properties ?? []).forEach((p) => printFunction(p)); + this.printCombinedProprs( + objectsCollection.find((x) => x.name === y), + writer, + objectsCollection, + printFunction + ); + }); + } } diff --git a/src/generators/models-generator/InterfacesGenerator.ts b/src/generators/models-generator/InterfacesGenerator.ts index ad42d24..11265f8 100644 --- a/src/generators/models-generator/InterfacesGenerator.ts +++ b/src/generators/models-generator/InterfacesGenerator.ts @@ -1,15 +1,42 @@ -import { InterfaceDeclarationStructure, OptionalKind, PropertySignatureStructure, StructureKind } from 'ts-morph'; +import { + InterfaceDeclarationStructure, + OptionalKind, + PropertySignatureStructure, + StructureKind, + TypeAliasDeclarationStructure +} from 'ts-morph'; + import { IInterfaceModel, IInterfacePropertyModel } from '../../models/InterfaceModel'; import { TypeSerializer } from '../utils/TypeSerializer'; export class InterfacesGenerator { - public getCodeStructure(interfaces: IInterfaceModel[]): InterfaceDeclarationStructure[] { - return interfaces.map((z) => ({ - kind: StructureKind.Interface, - name: z.name, - isExported: true, - properties: z.properties.map((x) => this.getInterfaceProperty(x)) - })); + public getCodeStructure(interfaces: IInterfaceModel[]): (InterfaceDeclarationStructure | TypeAliasDeclarationStructure)[] { + const baseInterfaces: InterfaceDeclarationStructure[] = []; + const types: (InterfaceDeclarationStructure | TypeAliasDeclarationStructure)[] = interfaces.map((z) => { + if (z.combineInterfaces.length) { + const name = z.name + 'BaseInterface'; + baseInterfaces.push({ + kind: StructureKind.Interface, + name: name, + isExported: false, + properties: z.properties.map((x) => this.getInterfaceProperty(x)) + }); + return { + kind: StructureKind.TypeAlias, + type: name + ' & ' + z.combineInterfaces.join(' & '), + name: z.name, + isExported: true + }; + } else { + return { + kind: StructureKind.Interface, + name: z.name, + isExported: true, + properties: z.properties.map((x) => this.getInterfaceProperty(x)) + }; + } + }); + return [...baseInterfaces, ...types]; } protected getInterfaceProperty(model: IInterfacePropertyModel): OptionalKind { diff --git a/src/generators/utils/is-defined.ts b/src/generators/utils/is-defined.ts new file mode 100644 index 0000000..d375aee --- /dev/null +++ b/src/generators/utils/is-defined.ts @@ -0,0 +1,3 @@ +export function isDefined(value: T | undefined): value is T { + return value !== undefined; +} diff --git a/src/models/InterfaceModel.ts b/src/models/InterfaceModel.ts index 2ffc513..468293d 100644 --- a/src/models/InterfaceModel.ts +++ b/src/models/InterfaceModel.ts @@ -8,4 +8,5 @@ export interface IInterfacePropertyModel { export interface IInterfaceModel { name: string; properties: IInterfacePropertyModel[]; + combineInterfaces: string[]; } diff --git a/src/models/ObjectModel.ts b/src/models/ObjectModel.ts index ff64ecd..5d89ff3 100644 --- a/src/models/ObjectModel.ts +++ b/src/models/ObjectModel.ts @@ -1,3 +1,4 @@ +import { IOpenAPI3Reference } from '../swagger/v3/reference'; import { IType } from './TypeModel'; export interface IObjectPropertyModel extends IType { @@ -11,4 +12,6 @@ export interface IObjectModel { dtoType: string; isNullable: boolean; properties: IObjectPropertyModel[]; + combineTypes: string[]; + combineTypesRefs: IOpenAPI3Reference[]; } diff --git a/src/services/ModelMappingService.ts b/src/services/ModelMappingService.ts index df89ea7..361ba3e 100644 --- a/src/services/ModelMappingService.ts +++ b/src/services/ModelMappingService.ts @@ -12,11 +12,14 @@ import { IOpenAPI3GuidSchema } from '../swagger/v3/schemas/guid-schema'; import { IOpenAPI3ObjectSchema } from '../swagger/v3/schemas/object-schema'; import { OpenAPI3Schema, OpenAPI3SchemaContainer, OpenAPI3SimpleSchema } from '../swagger/v3/schemas/schema'; import { first, sortBy } from '../utils'; +import { isDefined } from '../generators/utils/is-defined'; import { TypesService } from './TypesService'; const IGNORE_PROPERTIES = ['startRow', 'rowCount']; export class ModelMappingService { + public additionalObjects: IObjectModel[] = []; + public additionalEnums: IEnumModel[] = []; constructor( private readonly openAPIService: OpenAPIService, private readonly typesGuard: OpenAPITypesGuard, @@ -53,11 +56,15 @@ export class ModelMappingService { } }); + objects.forEach((x) => this.addCombineObjectsByRefs(x)); + return { - enums: enums.sort(sortBy((z) => z.name)), + enums: this.getUnicItemsByProp('name', ...this.additionalEnums, ...enums).sort(sortBy((z) => z.name)), identities: identities.sort(sortBy((z) => z.name)), - interfaces: this.getInterfaces(identities, objects).sort(sortBy((z) => z.name)), - objects: objects.sort(sortBy((z) => z.name)) + interfaces: this.getInterfaces(identities, this.getUnicItemsByProp('name', ...this.additionalObjects, ...objects)).sort( + sortBy((z) => z.name) + ), + objects: this.getUnicItemsByProp('name', ...this.additionalObjects, ...objects).sort(sortBy((z) => z.name)) }; } @@ -75,7 +82,17 @@ export class ModelMappingService { } private toObjectModel(name: string, schema: IOpenAPI3ObjectSchema): IObjectModel { - const model: IObjectModel = { name, isNullable: schema.nullable ?? false, dtoType: this.getInterfaceName(name), properties: [] }; + const model: IObjectModel = { + name, + isNullable: schema.nullable ?? false, + dtoType: this.getInterfaceName(name), + properties: [], + combineTypes: [], + combineTypesRefs: [] + }; + + this.addCombineTypes(schema, model); + if (!schema.properties) { return model; } @@ -101,6 +118,9 @@ export class ModelMappingService { } else if (this.typesGuard.isReference(schema.items)) { property = this.getReferenceProperty(name, schema.items); } + if (this.typesGuard.isOneOf(schema.items)) { + property = this.getReferenceProperty(name, first(schema.items.oneOf)); + } if (property) { property.isCollection = true; @@ -113,6 +133,8 @@ export class ModelMappingService { property = this.getReferenceProperty(name, schema); } else if (this.typesGuard.isAllOf(schema)) { property = this.getReferenceProperty(name, first(schema.allOf)); + } else if (this.typesGuard.isOneOf(schema)) { + property = this.getReferenceProperty(name, first(schema.oneOf)); } if (property) { @@ -121,6 +143,64 @@ export class ModelMappingService { } } + private addCombineTypes(schema: IOpenAPI3ObjectSchema, model: IObjectModel): void { + if (!this.typesGuard.isAllOf(schema)) { + return; + } + schema.allOf.forEach((x) => { + const refSchema = this.openAPIService.getRefSchema(x); + const schemaKey = this.openAPIService.getSchemaKey(x); + if (this.typesGuard.isObject(refSchema)) { + model.combineTypes = [...model.combineTypes, schemaKey]; + model.combineTypesRefs = [...model.combineTypesRefs, x]; + } + }); + } + + private addCombineObjectsByRefs(model: IObjectModel): void { + if (!model.combineTypesRefs) { + return; + } + model.combineTypesRefs.forEach((ref) => this.addCombineObjectsByRef(ref)); + + if (!model.properties) { + return; + } + model.properties.forEach((prop) => this.addPropertiesCombineType(prop)); + } + + private addCombineObjectsByRef(ref: IOpenAPI3Reference): void { + const refSchema = this.openAPIService.getRefSchema(ref); + const schemaKey = this.openAPIService.getSchemaKey(ref); + if (this.typesGuard.isObject(refSchema)) { + const combinedModel = this.toObjectModel(schemaKey, refSchema); + + if (this.additionalObjects.find((x) => x.name === combinedModel.name)) { + //нужно ли помержить юнион типы тут + return; + } + + this.additionalObjects.push(combinedModel); + this.addCombineObjectsByRefs(combinedModel); + } + } + + private addPropertiesCombineType(prop: IObjectPropertyModel): void { + const ref = { $ref: '#/components/schemas/' + prop.type }; + if (prop.kind === PropertyKind.Object) { + this.addCombineObjectsByRef(ref); + } else if (prop.kind === PropertyKind.Enum) { + const refSchema = this.openAPIService.getRefSchema(ref); + const schemaKey = this.openAPIService.getSchemaKey(ref); + if (this.typesGuard.isEnum(refSchema)) { + const emun = this.toEnumModel(schemaKey, refSchema); + if (!this.additionalEnums.find((e) => e.name === emun.name)) { + this.additionalEnums.push(emun); + } + } + } + } + private getSimpleProperty(name: string, schema: OpenAPI3SimpleSchema): IObjectPropertyModel { return { ...this.typesService.getSimpleType(schema), @@ -160,12 +240,14 @@ export class ModelMappingService { private getInterfaces(identities: IIdentityModel[], objects: IObjectModel[]): IInterfaceModel[] { const interfaces: IInterfaceModel[] = identities.map((z) => ({ name: this.getInterfaceName(z.name), - properties: [{ name: z.property.name, dtoType: z.property.dtoType, isCollection: false, isNullable: false }] + properties: [{ name: z.property.name, dtoType: z.property.dtoType, isCollection: false, isNullable: false }], + combineInterfaces: [] })); return interfaces.concat( objects.map((z) => ({ name: this.getInterfaceName(z.name), + combineInterfaces: z.combineTypes.map((x) => this.getInterfaceName(x)), properties: z.properties.map((x) => ({ name: x.name, dtoType: x.dtoType, @@ -186,4 +268,9 @@ export class ModelMappingService { } return schema.properties && Object.keys(schema.properties)?.length === 1 && this.typesGuard.isGuid(schema.properties['id']); } + + private getUnicItemsByProp(key: T, ...array1: T2[]): T2[] { + const unionKeys = [...new Set(array1.map((x) => x[key]))]; + return unionKeys.map((x) => array1.find((y) => y[key] === x)).filter(isDefined); + } } diff --git a/src/swagger/OpenAPIService.ts b/src/swagger/OpenAPIService.ts index 35e5478..03f53c9 100644 --- a/src/swagger/OpenAPIService.ts +++ b/src/swagger/OpenAPIService.ts @@ -20,7 +20,10 @@ interface IOperation { export type IOpenAPI3Operations = { [key: string]: { method: MethodOperation; operation: IOpenAPI3Operation }[] }; export class OpenAPIService { - constructor(private readonly spec: IOpenAPI3, private readonly typesGuard: OpenAPITypesGuard) { + constructor( + private readonly spec: IOpenAPI3, + private readonly typesGuard: OpenAPITypesGuard + ) { const majorVersion = this.majorVersion; if (majorVersion !== SUPPORTED_VERSION.toString()) { throw new Error(`Only OpenApi version ${SUPPORTED_VERSION} supported yet.`); diff --git a/src/swagger/OpenAPITypesGuard.ts b/src/swagger/OpenAPITypesGuard.ts index 19008b9..c039167 100644 --- a/src/swagger/OpenAPITypesGuard.ts +++ b/src/swagger/OpenAPITypesGuard.ts @@ -7,6 +7,7 @@ import { IOpenAPI3EnumSchema } from './v3/schemas/enum-schema'; import { IOpenAPI3GuidSchema } from './v3/schemas/guid-schema'; import { IOpenAPI3NumberSchema } from './v3/schemas/number-schema'; import { IOpenAPI3ObjectSchema } from './v3/schemas/object-schema'; +import { IOpenAPI3OneOfSchema } from './v3/schemas/one-of-schema'; import { OpenAPI3SimpleSchema } from './v3/schemas/schema'; import { IOpenAPI3StringSchema } from './v3/schemas/string-schema'; @@ -17,6 +18,7 @@ type SchemaType = | IOpenAPI3ObjectSchema | IOpenAPI3EnumSchema | IOpenAPI3AllOfSchema + | IOpenAPI3OneOfSchema | undefined; export class OpenAPITypesGuard { @@ -40,6 +42,10 @@ export class OpenAPITypesGuard { return Boolean((schema as IOpenAPI3AllOfSchema)?.allOf); } + public isOneOf(schema: SchemaType): schema is IOpenAPI3OneOfSchema { + return Boolean((schema as IOpenAPI3OneOfSchema).oneOf); + } + public isEnum(schema: SchemaType): schema is IOpenAPI3EnumSchema { const enumSchema = schema as IOpenAPI3EnumSchema; if (!schema) { diff --git a/src/swagger/v3/schemas/one-of-schema.ts b/src/swagger/v3/schemas/one-of-schema.ts new file mode 100644 index 0000000..99cead3 --- /dev/null +++ b/src/swagger/v3/schemas/one-of-schema.ts @@ -0,0 +1,6 @@ +import { IOpenAPI3Reference } from '../reference'; +import { IOpenAPI3BaseSchema } from './base-schema'; + +export interface IOpenAPI3OneOfSchema extends IOpenAPI3BaseSchema { + oneOf: IOpenAPI3Reference[]; +}