Skip to content

Commit

Permalink
allof support
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrey Klimenko committed Mar 14, 2024
1 parent 8973f56 commit b5d6b50
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 29 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
71 changes: 56 additions & 15 deletions src/generators/ModelsGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ClassDeclarationStructure,
CodeBlockWriter,
ConstructorDeclarationStructure,
ImportDeclarationStructure,
OptionalKind,
Expand Down Expand Up @@ -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,
Expand All @@ -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('};');
}
},
Expand All @@ -152,32 +156,49 @@ 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;');
}
}
]
}));
}

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,
Expand Down Expand Up @@ -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
);
});
}
}
43 changes: 35 additions & 8 deletions src/generators/models-generator/InterfacesGenerator.ts
Original file line number Diff line number Diff line change
@@ -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<PropertySignatureStructure> {
Expand Down
3 changes: 3 additions & 0 deletions src/generators/utils/is-defined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isDefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
1 change: 1 addition & 0 deletions src/models/InterfaceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface IInterfacePropertyModel {
export interface IInterfaceModel {
name: string;
properties: IInterfacePropertyModel[];
combineInterfaces: string[];
}
3 changes: 3 additions & 0 deletions src/models/ObjectModel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IOpenAPI3Reference } from '../swagger/v3/reference';
import { IType } from './TypeModel';

export interface IObjectPropertyModel extends IType {
Expand All @@ -11,4 +12,6 @@ export interface IObjectModel {
dtoType: string;
isNullable: boolean;
properties: IObjectPropertyModel[];
combineTypes: string[];
combineTypesRefs: IOpenAPI3Reference[];
}
97 changes: 92 additions & 5 deletions src/services/ModelMappingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
};
}

Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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),
Expand Down Expand Up @@ -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,
Expand All @@ -186,4 +268,9 @@ export class ModelMappingService {
}
return schema.properties && Object.keys(schema.properties)?.length === 1 && this.typesGuard.isGuid(schema.properties['id']);
}

private getUnicItemsByProp<T extends string, T2 extends { [key in T]: string }>(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);
}
}
5 changes: 4 additions & 1 deletion src/swagger/OpenAPIService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
Expand Down
6 changes: 6 additions & 0 deletions src/swagger/OpenAPITypesGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -17,6 +18,7 @@ type SchemaType =
| IOpenAPI3ObjectSchema
| IOpenAPI3EnumSchema
| IOpenAPI3AllOfSchema
| IOpenAPI3OneOfSchema
| undefined;

export class OpenAPITypesGuard {
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit b5d6b50

Please sign in to comment.