diff --git a/.snapshot/all/models.ts b/.snapshot/all/models.ts index 99865bb..caf850c 100644 --- a/.snapshot/all/models.ts +++ b/.snapshot/all/models.ts @@ -34,15 +34,16 @@ interface ICategoryMotorsDtoBaseInterface { export type ICategoryMotorsDto = ICategoryMotorsDtoBaseInterface & ICategory; export interface IProduct { - categories: $types.TypeOrUndefined; + categories: ICategoryUnion[]; category: $types.TypeOrUndefined; colors: $types.TypeOrUndefined; expireDate: $types.TypeOrUndefined; externalId: $types.TypeOrUndefinedNullable; id: $types.TypeOrUndefined; - modifyDates: $types.TypeOrUndefined; + modifyDates: string[]; name: $types.TypeOrUndefinedNullable; - status: $types.TypeOrUndefined; + parentProduct: IProduct; + status: ProductStatus; } export interface IProductIdentityDTO { @@ -101,11 +102,16 @@ export class ProductIdentityDTO { } export class Category { - public name: $types.TypeOrUndefinedNullable = undefined; - public type: $types.TypeOrUndefined = undefined; + public name: $types.TypeOrUndefinedNullable; + public type: $types.TypeOrUndefined; private __category!: string; - public static toDTO(model: Partial): ICategory { + protected constructor(dto: ICategory) { + this.name = dto.name; + this.type = dto.type; + } + + public static toDTO(model: $types.PublicFields): ICategory { return { name: model.name, type: model.type, @@ -113,20 +119,23 @@ export class Category { } public static fromDTO(dto: ICategory): Category { - const model = new Category(); - model.name = dto.name; - model.type = dto.type; - return model; + return new Category(dto); } } export class CategoryElectronicsDto { - public name: $types.TypeOrUndefinedNullable = undefined; - public type: $types.TypeOrUndefined = undefined; - public syntheticTest: $types.TypeOrUndefinedNullable = undefined; + public name: $types.TypeOrUndefinedNullable; + public type: $types.TypeOrUndefined; + public syntheticTest: $types.TypeOrUndefinedNullable; private __categoryElectronicsDto!: string; - public static toDTO(model: Partial): ICategoryElectronicsDto { + protected constructor(dto: ICategoryElectronicsDto) { + this.syntheticTest = dto.syntheticTest; + this.name = dto.name; + this.type = dto.type; + } + + public static toDTO(model: $types.PublicFields): ICategoryElectronicsDto { return { syntheticTest: model.syntheticTest, name: model.name, @@ -135,21 +144,23 @@ export class CategoryElectronicsDto { } public static fromDTO(dto: ICategoryElectronicsDto): CategoryElectronicsDto { - const model = new CategoryElectronicsDto(); - model.syntheticTest = dto.syntheticTest; - model.name = dto.name; - model.type = dto.type; - return model; + return new CategoryElectronicsDto(dto); } } export class CategoryMotorsDto { - public name: $types.TypeOrUndefinedNullable = undefined; - public type: $types.TypeOrUndefined = undefined; - public volume: $types.TypeOrUndefinedNullable = undefined; + public name: $types.TypeOrUndefinedNullable; + public type: $types.TypeOrUndefined; + public volume: $types.TypeOrUndefinedNullable; private __categoryMotorsDto!: string; - public static toDTO(model: Partial): ICategoryMotorsDto { + protected constructor(dto: ICategoryMotorsDto) { + this.volume = dto.volume; + this.name = dto.name; + this.type = dto.type; + } + + public static toDTO(model: $types.PublicFields): ICategoryMotorsDto { return { volume: model.volume, name: model.name, @@ -158,51 +169,52 @@ export class CategoryMotorsDto { } public static fromDTO(dto: ICategoryMotorsDto): CategoryMotorsDto { - const model = new CategoryMotorsDto(); - model.volume = dto.volume; - model.name = dto.name; - model.type = dto.type; - return model; + return new CategoryMotorsDto(dto); } } export class Product { - public categories: CategoryUnion[] = []; - public category: $types.TypeOrUndefined = undefined; - public colors: string[] = []; - public expireDate: $types.TypeOrUndefined = undefined; - public externalId: $types.TypeOrUndefinedNullable = undefined; - public id: $types.TypeOrUndefined = undefined; - public modifyDates: Date[] = []; - public name: $types.TypeOrUndefinedNullable = undefined; - public status: $types.TypeOrUndefined = undefined; + public categories: CategoryUnion[]; + public category: $types.TypeOrUndefined; + public colors: string[]; + public expireDate: $types.TypeOrUndefined; + public externalId: $types.TypeOrUndefinedNullable; + public id: $types.TypeOrUndefined; + public modifyDates: Date[]; + public name: $types.TypeOrUndefinedNullable; + public parentProduct: Product; + public status: ProductStatus; private __product!: string; - public static toDTO(model: Partial): IProduct { + protected constructor(dto: IProduct) { + this.categories = dto.categories.map(x => CategoryUnionClass.fromDTO(x)); + this.category = dto.category ? CategoryUnionClass.fromDTO(dto.category) : undefined; + this.colors = dto.colors ? dto.colors : []; + this.expireDate = toDateIn(dto.expireDate); + this.externalId = dto.externalId ? new Guid(dto.externalId) : null; + this.id = new Guid(dto.id); + this.modifyDates = dto.modifyDates.map(toDateIn); + this.name = dto.name; + this.parentProduct = Product.fromDTO(dto.parentProduct); + this.status = dto.status; + } + + public static toDTO(model: $types.PublicFields): IProduct { return { - categories: model.categories ? model.categories.map(x => CategoryUnionClass.toDTO(x)) : undefined, + categories: model.categories.map(x => CategoryUnionClass.toDTO(x)), category: model.category ? CategoryUnionClass.toDTO(model.category) : undefined, colors: model.colors, expireDate: toDateOut(model.expireDate), externalId: model.externalId ? model.externalId.toString() : null, id: model.id ? model.id.toString() : Guid.empty.toString(), - modifyDates: model.modifyDates ? model.modifyDates.map(toDateOut) : undefined, + modifyDates: model.modifyDates.map(toDateOut), name: model.name, + parentProduct: Product.toDTO(model.parentProduct), status: model.status, }; } public static fromDTO(dto: IProduct): Product { - const model = new Product(); - model.categories = dto.categories ? dto.categories.map(x => CategoryUnionClass.fromDTO(x)) : []; - model.category = dto.category ? CategoryUnionClass.fromDTO(dto.category) : undefined; - model.colors = dto.colors ? dto.colors : []; - model.expireDate = toDateIn(dto.expireDate); - model.externalId = dto.externalId ? new Guid(dto.externalId) : null; - model.id = new Guid(dto.id); - model.modifyDates = dto.modifyDates ? dto.modifyDates.map(toDateIn) : []; - model.name = dto.name; - model.status = dto.status; - return model; + return new Product(dto); } } diff --git a/.snapshot/withLegacyUndefineGeneration/models.ts b/.snapshot/withLegacyUndefineGeneration/models.ts new file mode 100644 index 0000000..ef57bd9 --- /dev/null +++ b/.snapshot/withLegacyUndefineGeneration/models.ts @@ -0,0 +1,220 @@ +import { Guid } from './Guid'; +import { toDateIn, toDateOut } from './date-converters'; +import type * as $types from './types'; + +export enum CategoryUnionTypes { + CategoryElectronicsDto = '1', + CategoryMotorsDto = '2' +} + +export enum ProductStatus { + InStock = 0, + OutOfStock = -1, + UnderTheOrder = 1 +} + +export type CategoryUnion = Category | CategoryElectronicsDto | CategoryMotorsDto; +export type ICategoryUnion = ICategory | ICategoryElectronicsDto | ICategoryMotorsDto; + +export interface ICategory { + name: $types.TypeOrUndefinedNullable; + type: $types.TypeOrUndefined; +} + +interface ICategoryElectronicsDtoBaseInterface { + syntheticTest: $types.TypeOrUndefinedNullable; +} + +export type ICategoryElectronicsDto = ICategoryElectronicsDtoBaseInterface & ICategory; + +interface ICategoryMotorsDtoBaseInterface { + volume: $types.TypeOrUndefinedNullable; +} + +export type ICategoryMotorsDto = ICategoryMotorsDtoBaseInterface & ICategory; + +export interface IProduct { + categories: $types.TypeOrUndefined; + category: $types.TypeOrUndefined; + colors: $types.TypeOrUndefined; + expireDate: $types.TypeOrUndefined; + externalId: $types.TypeOrUndefinedNullable; + id: $types.TypeOrUndefined; + modifyDates: $types.TypeOrUndefined; + name: $types.TypeOrUndefinedNullable; + parentProduct: $types.TypeOrUndefinedNullable; + status: $types.TypeOrUndefined; +} + +export interface IProductIdentityDTO { + id: $types.TypeOrUndefined; +} + +export class CategoryUnionClass { + public static fromDTO(dto: ICategoryUnion): CategoryUnion { + if (this.isCategoryElectronicsDto(dto)) { + return CategoryElectronicsDto.fromDTO(dto); + } + if (this.isCategoryMotorsDto(dto)) { + return CategoryMotorsDto.fromDTO(dto); + } + return Category.fromDTO(dto); + } + + public static toDTO(model: CategoryUnion): ICategoryUnion { + if (this.isICategoryElectronicsDto(model)) { + return CategoryElectronicsDto.toDTO(model); + } + if (this.isICategoryMotorsDto(model)) { + return CategoryMotorsDto.toDTO(model); + } + return Category.toDTO(model); + } + + private static isCategoryElectronicsDto(dto: ICategoryUnion): dto is ICategoryElectronicsDto { + return dto.type === CategoryUnionTypes.CategoryElectronicsDto; + } + + private static isCategoryMotorsDto(dto: ICategoryUnion): dto is ICategoryMotorsDto { + return dto.type === CategoryUnionTypes.CategoryMotorsDto; + } + + private static isICategoryElectronicsDto(dto: CategoryUnion): dto is CategoryElectronicsDto { + return dto.type === CategoryUnionTypes.CategoryElectronicsDto; + } + + private static isICategoryMotorsDto(dto: CategoryUnion): dto is CategoryMotorsDto { + return dto.type === CategoryUnionTypes.CategoryMotorsDto; + } +} + +export class ProductIdentityDTO { + public id: Guid; + private __productIdentityDTO!: string; + + constructor(id?: $types.TypeOrUndefined) { + this.id = new Guid(id); + } + + public static toDTO(id: Guid): IProductIdentityDTO { + return { id: id.toString() }; + } +} + +export class Category { + public name: $types.TypeOrUndefinedNullable; + public type: $types.TypeOrUndefined; + private __category!: string; + + protected constructor(dto: ICategory) { + this.name = dto.name; + this.type = dto.type; + } + + public static toDTO(model: Partial): ICategory { + return { + name: model.name, + type: model.type, + }; + } + + public static fromDTO(dto: ICategory): Category { + return new Category(dto); + } +} + +export class CategoryElectronicsDto { + public name: $types.TypeOrUndefinedNullable; + public type: $types.TypeOrUndefined; + public syntheticTest: $types.TypeOrUndefinedNullable; + private __categoryElectronicsDto!: string; + + protected constructor(dto: ICategoryElectronicsDto) { + this.syntheticTest = dto.syntheticTest; + this.name = dto.name; + this.type = dto.type; + } + + public static toDTO(model: Partial): ICategoryElectronicsDto { + return { + syntheticTest: model.syntheticTest, + name: model.name, + type: model.type, + }; + } + + public static fromDTO(dto: ICategoryElectronicsDto): CategoryElectronicsDto { + return new CategoryElectronicsDto(dto); + } +} + +export class CategoryMotorsDto { + public name: $types.TypeOrUndefinedNullable; + public type: $types.TypeOrUndefined; + public volume: $types.TypeOrUndefinedNullable; + private __categoryMotorsDto!: string; + + protected constructor(dto: ICategoryMotorsDto) { + this.volume = dto.volume; + this.name = dto.name; + this.type = dto.type; + } + + public static toDTO(model: Partial): ICategoryMotorsDto { + return { + volume: model.volume, + name: model.name, + type: model.type, + }; + } + + public static fromDTO(dto: ICategoryMotorsDto): CategoryMotorsDto { + return new CategoryMotorsDto(dto); + } +} + +export class Product { + public categories: CategoryUnion[]; + public category: $types.TypeOrUndefined; + public colors: string[]; + public expireDate: $types.TypeOrUndefined; + public externalId: $types.TypeOrUndefinedNullable; + public id: $types.TypeOrUndefined; + public modifyDates: Date[]; + public name: $types.TypeOrUndefinedNullable; + public parentProduct: $types.TypeOrUndefinedNullable; + public status: $types.TypeOrUndefined; + private __product!: string; + + protected constructor(dto: IProduct) { + this.categories = dto.categories ? dto.categories.map(x => CategoryUnionClass.fromDTO(x)) : []; + this.category = dto.category ? CategoryUnionClass.fromDTO(dto.category) : undefined; + this.colors = dto.colors ? dto.colors : []; + this.expireDate = toDateIn(dto.expireDate); + this.externalId = dto.externalId ? new Guid(dto.externalId) : null; + this.id = new Guid(dto.id); + this.modifyDates = dto.modifyDates ? dto.modifyDates.map(toDateIn) : []; + this.name = dto.name; + this.parentProduct = dto.parentProduct ? Product.fromDTO(dto.parentProduct) : undefined; + this.status = dto.status; + } + + public static toDTO(model: Partial): IProduct { + return { + categories: model.categories ? model.categories.map(x => CategoryUnionClass.toDTO(x)) : undefined, + category: model.category ? CategoryUnionClass.toDTO(model.category) : undefined, + colors: model.colors, + expireDate: toDateOut(model.expireDate), + externalId: model.externalId ? model.externalId.toString() : null, + id: model.id ? model.id.toString() : Guid.empty.toString(), + modifyDates: model.modifyDates ? model.modifyDates.map(toDateOut) : undefined, + name: model.name, + parentProduct: model.parentProduct ? Product.toDTO(model.parentProduct) : undefined, + status: model.status, + }; + } + + public static fromDTO(dto: IProduct): Product { + return new Product(dto); + } +} diff --git a/README.md b/README.md index 7bb2212..152f4c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GenGen - [![NPM version](https://img.shields.io/npm/v/@luxbss/gengen.svg)](https://www.npmjs.com/package/@luxbss/gengen) [![license](https://img.shields.io/github/license/luxoft/gengen)](https://github.com/Luxoft/gengen/blob/master/LICENSE.txt) [![GitHub contributors](https://img.shields.io/github/contributors/luxoft/gengen)](https://github.com/Luxoft/gengen/graphs/contributors/) +[![NPM version](https://img.shields.io/npm/v/@luxbss/gengen.svg)](https://www.npmjs.com/package/@luxbss/gengen) [![license](https://img.shields.io/github/license/luxoft/gengen)](https://github.com/Luxoft/gengen/blob/master/LICENSE.txt) [![GitHub contributors](https://img.shields.io/github/contributors/luxoft/gengen)](https://github.com/Luxoft/gengen/graphs/contributors/) This tool generates models and [Angular](https://angular.io/) services based on generated [Swagger JSON](https://swagger.io/specification/). @@ -45,18 +45,18 @@ gengen g --all ### Options -| Option | Description | Type | Default value | -| ---------------------- | ------------------------------------------------------------------------------------------ | ------- | ---------------------------------------------- | -| **all** | Generate all | boolean | false | -| **url** | Location of swagger.json | string | https://localhost:5001/swagger/v1/swagger.json | -| **file** | Local path to swagger.json | string | | -| **output** | Output directory | string | ./src/generated | -| **configOutput** | Output directory using in 'Generate a part of API' scenario | string | ./.generated | -| **aliasName** | Specify prefix for generated filenames. [more info](#aliasName) | string | | -| **withRequestOptions** | Allows to pass http request options to generated methods. [more info](#withRequestOptions) | boolean | false | -| **utilsRelativePath** | Relative path to utils files. It may be useful when you have multiple generation sources | string | | -| **unstrictId** | Disable converting 'id' properties to strong Guid type. [more info](#unstrictId) | boolean | false | -| | +| Option | Description | Type | Default value | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------- | +| **all** | Generate all | boolean | false | +| **url** | Location of swagger.json | string | https://localhost:5001/swagger/v1/swagger.json | +| **file** | Local path to swagger.json | string | | +| **output** | Output directory | string | ./src/generated | +| **configOutput** | Output directory using in 'Generate a part of API' scenario | string | ./.generated | +| **aliasName** | Specify prefix for generated filenames. [more info](#aliasName) | string | | +| **withRequestOptions** | Allows to pass http request options to generated methods. [more info](#withRequestOptions) | boolean | false | +| **utilsRelativePath** | Relative path to utils files. It may be useful when you have multiple generation sources | string | | +| **unstrictId** | Disable converting 'id' properties to strong Guid type. [more info](#unstrictId) | boolean | false | +| **withLegacyUndefineGeneration** | Allow enabling the "previous" style generation with many undefined types. [more info](#withLegacyUndefineGeneration) | boolean | false | ### Option details @@ -110,9 +110,8 @@ export class ExampleService extends BaseHttpService { // ... } -@Component( - // ... -) +@Component() +// ... export class MyComponent { constructor(private exampleService: ExampleService) { this.exampleService.methodName({ @@ -144,6 +143,44 @@ public static fromDTO(dto: IProduct): Product { } ``` +#### withLegacyUndefineGeneration + +From version 1.3, GenGen supports the [required properties](https://swagger.io/docs/specification/v3_0/data-models/data-types/#required-properties) option from types, which indicates that a field is required on the backend and mandatory (not-null) in the type. For `required` fields, GenGen uses non-null and non-undefined types. Additionally, the generation of the toDto methods has been changed to use `PublicFields` instead of `Partial`. + +Example: + +```ts +// with withLegacyUndefineGeneration +public static toDTO(model: Partial): IProduct { + return { + // ... + id: model.obj ? ObjDto.toDTO(model.obj) : undefined, + // ... + }; +} + +// default, all field in model required +public static toDTO(model: PublicFields): IProduct { + return { + // ... + id: ObjDto.toDTO(model.obj), + // ... + }; +} +``` + +To enable all power of this behavior you need to customize generation your OpenAPI specification, for Swashbuckle.AspNetCore it can be done with the next [options](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2036): + +```csharp +serviceCollection.AddSwaggerGen(builder => +{ + builder.SupportNonNullableReferenceTypes(); + builder.NonNullableReferenceTypesAsRequired(); +}); +``` + +However, if you want to use the previous generation for some reason, you can enable it with the `withLegacyUndefineGeneration` option. + # License and copyright Copyright (c) 2020-2023 Luxoft diff --git a/__tests__/generators/utils/TypeSerializer.spec.ts b/__tests__/generators/utils/TypeSerializer.spec.ts index ec57b97..8f297a7 100644 --- a/__tests__/generators/utils/TypeSerializer.spec.ts +++ b/__tests__/generators/utils/TypeSerializer.spec.ts @@ -63,20 +63,51 @@ describe('TypeSerializer tests', () => { }); }); - test('fromInterfaceProperty should return optional dtoType', () => { + test('fromInterfaceProperty without isRequired should return optional dtoType', () => { // Arrange // Act const result = TypeSerializer.fromInterfaceProperty({ dtoType: 'MyDtoType', isCollection: false, isNullable: false, - name: 'interface property name' + name: 'interface property name', + isRequired: false }).toString(); // Assert expect(result).toEqual('$types.TypeOrUndefined'); }); + test('fromInterfaceProperty with isRequired should return dtoType', () => { + // Arrange + // Act + const result = TypeSerializer.fromInterfaceProperty({ + dtoType: 'MyDtoType', + isCollection: false, + isNullable: false, + name: 'interface property name', + isRequired: true + }).toString(); + + // Assert + expect(result).toEqual('MyDtoType'); + }); + + test('fromInterfaceProperty with isRequired but isNullable should return dtoType', () => { + // Arrange + // Act + const result = TypeSerializer.fromInterfaceProperty({ + dtoType: 'MyDtoType', + isCollection: false, + isNullable: true, + name: 'interface property name', + isRequired: true + }).toString(); + + // Assert + expect(result).toEqual('MyDtoType'); + }); + test('fromTypeName should return optional type', () => { // Arrange // Act diff --git a/e2e/e2e.ts b/e2e/e2e.ts index 8c4fbb4..c4164ac 100644 --- a/e2e/e2e.ts +++ b/e2e/e2e.ts @@ -11,6 +11,11 @@ async function main() { snapshotter('./.snapshot/all/models.ts', './.output/all/models.ts', 'Models'); snapshotter('./.snapshot/all/services.ts', './.output/all/services.ts', 'Services without RequestOptions'); snapshotter('./.snapshot/withRequestOptions/services.ts', './.output/withRequestOptions/services.ts', 'Services with RequestOptions'); + snapshotter( + './.snapshot/withLegacyUndefineGeneration/models.ts', + './.output/withLegacyUndefineGeneration/models.ts', + 'Models with LegacyUndefineGeneration' + ); } async function snapshotter(pathA: string, pathB: string, name: string) { diff --git a/libs/date-converters.ts b/libs/date-converters.ts index 36cf751..faf5fe3 100644 --- a/libs/date-converters.ts +++ b/libs/date-converters.ts @@ -1,21 +1,21 @@ -import type { TypeOrUndefined, TypeOrUndefinedNullable } from './types'; +import type { TypeOrUndefinedNullable } from './types'; const HOUR_COEFF = 3600000; const MINUTS_IN_HOUR = 60; -export function toDateOut(value: TypeOrUndefinedNullable): TypeOrUndefined { +export function toDateOut>(value: T): T extends Date ? string : undefined { if (!value) { - return undefined; + return undefined as never; } - return dateOut(value).toISOString(); + return dateOut(value).toISOString() as never; } -export function toDateIn(value: TypeOrUndefinedNullable): TypeOrUndefined { +export function toDateIn>(value: T): T extends string ? Date : undefined { if (!value) { - return undefined; + return undefined as never; } - return dateIn(new Date(value)); + return dateIn(new Date(value)) as never; } function dateOut(value: Date): Date { diff --git a/libs/types.ts b/libs/types.ts index 0429050..64cebe7 100644 --- a/libs/types.ts +++ b/libs/types.ts @@ -1,3 +1,4 @@ export type TypeOrUndefined = T | undefined; export type TypeOrUndefinedNullable = T | undefined | null; -export type DTOIdentityType = { id: TypeOrUndefined }; \ No newline at end of file +export type PublicFields = Pick; +export type DTOIdentityType = { id: TypeOrUndefined }; diff --git a/package.json b/package.json index a3e9581..910d2a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@luxbss/gengen", - "version": "1.2.9", + "version": "1.3.0", "description": "Tool for generating models and Angular services based on OpenAPIs and Swagger's JSON", "bin": { "gengen": "./bin/index.js" @@ -12,8 +12,9 @@ "g:selected": "node ./bin/index.js g --file=./swagger.json --output=./.output/selected", "g": "node ./bin/index.js g --all --file=./swagger.json --output=./.output/all", "g:withRequestOptions": "node ./bin/index.js g --all --file=./swagger.json --output=./.output/withRequestOptions --withRequestOptions", + "g:withLegacyUndefineGeneration": "node ./bin/index.js g --all --file=./swagger.json --output=./.output/withLegacyUndefineGeneration --withLegacyUndefineGeneration", "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", + "e2e": "npm run g && npm run g:withRequestOptions && npm run g:withLegacyUndefineGeneration && ts-node ./e2e/e2e.ts", "g:b": "npm run build && npm run g", "test": "jest", "test:w": "jest --watch", diff --git a/src/bin.ts b/src/bin.ts index 7bdc0bb..2530c69 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -32,6 +32,7 @@ program .option('--all') .option('--withRequestOptions') .option('--unstrictId') + .option('--withLegacyUndefineGeneration') .description('Generates models and services') .action(async (params) => { const options = getOptions(params); diff --git a/src/generators/models-generator/ObjectGenerator.ts b/src/generators/models-generator/ObjectGenerator.ts index bb334a0..1b4d221 100644 --- a/src/generators/models-generator/ObjectGenerator.ts +++ b/src/generators/models-generator/ObjectGenerator.ts @@ -1,4 +1,11 @@ -import { ClassDeclarationStructure, CodeBlockWriter, PropertyDeclarationStructure, Scope, StructureKind } from 'ts-morph'; +import { + ClassDeclarationStructure, + CodeBlockWriter, + ConstructorDeclarationStructure, + PropertyDeclarationStructure, + Scope, + StructureKind +} from 'ts-morph'; import { PropertyKind } from '../../models/kinds/PropertyKind'; import { IExtendedObjectModel, IObjectPropertyModel, ObjectModel } from '../../models/ObjectModel'; @@ -7,6 +14,7 @@ import { FROM_DTO_METHOD, TO_DTO_METHOD } from '../ModelsGenerator'; import { ARRAY_STRING, NULL_STRING, UNDEFINED_STRING } from '../utils/consts'; import { TypeSerializer } from '../utils/TypeSerializer'; import { PropertiesGenerator } from './PropertiesGenerator'; +import { publicFields } from '../utils/typeOrUndefined'; export class ObjectGenerator { constructor( @@ -20,12 +28,25 @@ export class ObjectGenerator { isExported: true, name: z.name, properties: this.getObjectProperties(z, objects), + ctors: [ + { + kind: StructureKind.Constructor, + scope: Scope.Protected, + parameters: [{ name: 'dto', type: z.dtoType }], + statements: (x) => { + z.properties.forEach((p) => x.writeLine(`this.${p.name} = ${this.getFromDtoPropertyInitializer(p)};`)); + this.printCombinedProprs(z, x, objects, (p) => + x.writeLine(`this.${p.name} = ${this.getFromDtoPropertyInitializer(p)};`) + ); + } + } satisfies ConstructorDeclarationStructure + ], methods: [ { scope: Scope.Public, isStatic: true, name: TO_DTO_METHOD, - parameters: [{ name: 'model', type: `Partial<${z.name}>` }], + parameters: [{ name: 'model', type: this.getToDtoArgumentType(z.name) }], returnType: z.dtoType, statements: (x) => { x.writeLine('return {'); @@ -44,21 +65,16 @@ export class ObjectGenerator { name: FROM_DTO_METHOD, parameters: [{ name: 'dto', type: z.dtoType }], returnType: z.name, - statements: (x) => { - x.writeLine(`const model = new ${z.name}();`); - 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;'); - } + statements: (x) => x.writeLine(`return new ${z.name}(dto);`) } ] })); } + protected getToDtoArgumentType(originType: string): string { + return publicFields(originType); + } + private getObjectProperties(objectModel: ObjectModel, objects: ObjectModel[]): PropertyDeclarationStructure[] { return [ ...this.getObjectCombinedProperties(objectModel, objects), @@ -88,10 +104,11 @@ export class ObjectGenerator { name: objectProperty.name, type: new TypeSerializer({ type: { name: objectProperty.type }, - isNullable: objectProperty.isNullable, + isNullable: !objectProperty.isRequired && objectProperty.isNullable, + isOptional: !objectProperty.isRequired, isCollection: objectProperty.isCollection }).toString(), - initializer: objectProperty.isCollection ? ARRAY_STRING : UNDEFINED_STRING + initializer: undefined }; } @@ -101,45 +118,53 @@ export class ObjectGenerator { switch (property.kind) { case PropertyKind.Date: if (property.isCollection) { - return `${modelProperty} ? ${modelProperty}.map(toDateOut) : ${UNDEFINED_STRING}`; + const collectionMap = `${modelProperty}.map(toDateOut)`; + return property.isRequired ? collectionMap : `${modelProperty} ? ${collectionMap} : ${UNDEFINED_STRING}`; } return `toDateOut(${modelProperty})`; - case PropertyKind.Guid: + case PropertyKind.Guid: { if (property.isCollection) { - return `${modelProperty} ? ${modelProperty}.map(x => x.toString()) : ${UNDEFINED_STRING}`; + const collectionMap = `${modelProperty}.map(x => x.toString())`; + return property.isRequired ? collectionMap : `${modelProperty} ? ${collectionMap} : ${UNDEFINED_STRING}`; } - return ( - `${modelProperty} ? ${modelProperty}.toString()` + - ` : ${property.isNullable ? NULL_STRING : `${property.type}.empty.toString()`}` - ); + const valueMap = `${modelProperty}.toString()`; + return property.isRequired + ? valueMap + : `${modelProperty} ? ${valueMap} : ${property.isNullable ? NULL_STRING : `${property.type}.empty.toString()`}`; + } - case PropertyKind.Identity: + case PropertyKind.Identity: { if (property.isCollection) { - return `${modelProperty} ? ${modelProperty}.map(x => ${property.type}.${TO_DTO_METHOD}(x.id)) : ${UNDEFINED_STRING}`; + const collectionMap = `${modelProperty}.map(x => ${property.type}.${TO_DTO_METHOD}(x.id))`; + return property.isRequired ? collectionMap : `${modelProperty} ? ${collectionMap} : ${UNDEFINED_STRING}`; } - return `${modelProperty} ? ${property.type}.${TO_DTO_METHOD}(${modelProperty}.id) : ${UNDEFINED_STRING}`; + const valueMap = `${property.type}.${TO_DTO_METHOD}(${modelProperty}.id)`; + return property.isRequired ? valueMap : `${modelProperty} ? ${valueMap} : ${UNDEFINED_STRING}`; + } - case PropertyKind.Union: + case PropertyKind.Union: { if (property.isCollection) { - return `${modelProperty} ? ${modelProperty}.map(x => ${this.nameService.getClassName( - property.type - )}.${TO_DTO_METHOD}(x)) : ${UNDEFINED_STRING}`; + const collectionMap = `${modelProperty}.map(x => ${this.nameService.getClassName(property.type)}.${TO_DTO_METHOD}(x))`; + return property.isRequired ? collectionMap : `${modelProperty} ? ${collectionMap} : ${UNDEFINED_STRING}`; } - return `${modelProperty} ? ${this.nameService.getClassName( - property.type - )}.${TO_DTO_METHOD}(${modelProperty}) : ${UNDEFINED_STRING}`; + const valueMap = `${this.nameService.getClassName(property.type)}.${TO_DTO_METHOD}(${modelProperty})`; + return property.isRequired ? valueMap : `${modelProperty} ? ${valueMap} : ${UNDEFINED_STRING}`; + } - case PropertyKind.Object: + case PropertyKind.Object: { if (property.isCollection) { - return `${modelProperty} ? ${modelProperty}.map(x => ${property.type}.${TO_DTO_METHOD}(x)) : ${UNDEFINED_STRING}`; + const collectionMap = `${modelProperty}.map(x => ${property.type}.${TO_DTO_METHOD}(x))`; + return property.isRequired ? collectionMap : `${modelProperty} ? ${collectionMap} : ${UNDEFINED_STRING}`; } - return `${modelProperty} ? ${property.type}.${TO_DTO_METHOD}(${modelProperty}) : ${UNDEFINED_STRING}`; + const valueMap = `${property.type}.${TO_DTO_METHOD}(${modelProperty})`; + return property.isRequired ? valueMap : `${modelProperty} ? ${valueMap} : ${UNDEFINED_STRING}`; + } } return modelProperty; @@ -151,49 +176,56 @@ export class ObjectGenerator { switch (property.kind) { case PropertyKind.Date: if (property.isCollection) { - return `${dtoProperty} ? ${dtoProperty}.map(toDateIn) : ${ARRAY_STRING}`; + const collectionMap = `${dtoProperty}.map(toDateIn)`; + return property.isRequired ? collectionMap : `${dtoProperty} ? ${collectionMap} : ${ARRAY_STRING}`; } return `toDateIn(${dtoProperty})`; case PropertyKind.Guid: if (property.isCollection) { - return `${dtoProperty} ? ${dtoProperty}.map(x => new ${property.type}(x)) : ${ARRAY_STRING}`; + const collectionMap = ` ${dtoProperty}.map(x => new ${property.type}(x))`; + return property.isRequired ? collectionMap : `${dtoProperty} ? ${collectionMap} : ${ARRAY_STRING}`; } - if (property.isNullable) { + if (property.isNullable && !property.isRequired) { return `${dtoProperty} ? new ${property.type}(${dtoProperty}) : ${NULL_STRING}`; } return `new ${property.type}(${dtoProperty})`; - case PropertyKind.Identity: + case PropertyKind.Identity: { if (property.isCollection) { - return `${dtoProperty} ? ${dtoProperty}.map(x => new ${property.type}(x.id)) : ${ARRAY_STRING}`; + const collectionMap = `${dtoProperty}.map(x => new ${property.type}(x.id))`; + return property.isRequired ? collectionMap : `${dtoProperty} ? ${collectionMap} : ${ARRAY_STRING}`; } - return `${dtoProperty} ? new ${property.type}(${dtoProperty}.id) : ${UNDEFINED_STRING}`; + const createValue = `new ${property.type}(${dtoProperty}.id)`; + return property.isRequired ? createValue : `${dtoProperty} ? ${createValue} : ${UNDEFINED_STRING}`; + } - case PropertyKind.Union: + case PropertyKind.Union: { if (property.isCollection) { - return `${dtoProperty} ? ${dtoProperty}.map(x => ${this.nameService.getClassName( - property.type - )}.${FROM_DTO_METHOD}(x)) : ${ARRAY_STRING}`; + const collectionMap = `${dtoProperty}.map(x => ${this.nameService.getClassName(property.type)}.${FROM_DTO_METHOD}(x))`; + return property.isRequired ? collectionMap : `${dtoProperty} ? ${collectionMap} : ${ARRAY_STRING}`; } - return `${dtoProperty} ? ${this.nameService.getClassName( - property.type - )}.${FROM_DTO_METHOD}(${dtoProperty}) : ${UNDEFINED_STRING}`; + const createValue = `${this.nameService.getClassName(property.type)}.${FROM_DTO_METHOD}(${dtoProperty})`; + return property.isRequired ? createValue : `${dtoProperty} ? ${createValue} : ${UNDEFINED_STRING}`; + } - case PropertyKind.Object: + case PropertyKind.Object: { if (property.isCollection) { - return `${dtoProperty} ? ${dtoProperty}.map(x => ${property.type}.${FROM_DTO_METHOD}(x)) : ${ARRAY_STRING}`; + const collectionMap = `${dtoProperty}.map(x => ${property.type}.${FROM_DTO_METHOD}(x))`; + return property.isRequired ? collectionMap : `${dtoProperty} ? ${collectionMap} : ${ARRAY_STRING}`; } - return `${dtoProperty} ? ${property.type}.${FROM_DTO_METHOD}(${dtoProperty}) : ${UNDEFINED_STRING}`; + const valueMap = `${property.type}.${FROM_DTO_METHOD}(${dtoProperty})`; + return property.isRequired ? valueMap : `${dtoProperty} ? ${valueMap} : ${UNDEFINED_STRING}`; + } default: - if (property.isCollection) { + if (property.isCollection && !property.isRequired) { return `${dtoProperty} ? ${dtoProperty} : ${ARRAY_STRING}`; } diff --git a/src/generators/utils/TypeSerializer.ts b/src/generators/utils/TypeSerializer.ts index 0d5a3a1..7074834 100644 --- a/src/generators/utils/TypeSerializer.ts +++ b/src/generators/utils/TypeSerializer.ts @@ -18,7 +18,10 @@ export class TypeSerializer { public static fromInterfaceProperty(param: IInterfacePropertyModel): TypeSerializer { return new TypeSerializer({ isCollection: param.isCollection, - isNullable: param.isNullable, + // TODO: by design object model in strong typed (c#) languages can combine isRequired=true and isNullable=true, + // but it's a strange for UI contract, so any required field will count as Non-Nullable + isNullable: !param.isRequired && param.isNullable, + isOptional: !param.isRequired, type: { name: param.dtoType, isInterface: true diff --git a/src/generators/utils/typeOrUndefined.ts b/src/generators/utils/typeOrUndefined.ts index 56900ad..f0414f7 100644 --- a/src/generators/utils/typeOrUndefined.ts +++ b/src/generators/utils/typeOrUndefined.ts @@ -3,3 +3,7 @@ import { TYPES_NAMESPACE } from './consts'; export function typeOrUndefined(type: string): string { return `${TYPES_NAMESPACE}.TypeOrUndefined<${type}>`; } + +export function publicFields(type: string): string { + return `${TYPES_NAMESPACE}.PublicFields<${type}>`; +} diff --git a/src/gengen/GenGenCodeGenInjector.ts b/src/gengen/GenGenCodeGenInjector.ts index 460bd8f..a4f3c44 100644 --- a/src/gengen/GenGenCodeGenInjector.ts +++ b/src/gengen/GenGenCodeGenInjector.ts @@ -99,15 +99,12 @@ export class GenGenCodeGenInjector { this.options ) ) - .provide( - ModelMappingService, - (x) => new ModelMappingService(x.get(OpenAPIService), x.get(OpenAPITypesGuard), x.get(TypesService), x.get(NameService)) - ) + .provide(ModelMappingService, (x) => this.getModelMappingService(x)) .provide(TypesService, (x) => new TypesService(x.get(OpenAPITypesGuard), this.options)) .provide(OpenAPIService, (x) => new OpenAPIService(this.spec, x.get(OpenAPITypesGuard))) .provide(InterfacesGenerator, (x) => new InterfacesGenerator(x.get(PropertiesGenerator))) - .provide(ObjectGenerator, (x) => new ObjectGenerator(x.get(NameService), x.get(PropertiesGenerator))) + .provide(ObjectGenerator, (x) => this.getObjectGenerator(x)) .provide(UnionGenerator, (x) => new UnionGenerator(x.get(NameService))) .provide(IdentitiesGenerator, (x) => new IdentitiesGenerator(x.get(PropertiesGenerator), this.options)) .provide(PropertiesGenerator, (x) => new PropertiesGenerator(x.get(NameService))) @@ -117,4 +114,42 @@ export class GenGenCodeGenInjector { private options: IOptions, private spec: IOpenAPI3 ) {} + + private getObjectGenerator(injector: Injector): ObjectGenerator { + if (!this.options.withLegacyUndefineGeneration) { + return new ObjectGenerator(injector.get(NameService), injector.get(PropertiesGenerator)); + } + + class LegacyObjectGenerator extends ObjectGenerator { + protected override getToDtoArgumentType(x: string): string { + return `Partial<${x}>`; + } + } + + return new LegacyObjectGenerator(injector.get(NameService), injector.get(PropertiesGenerator)); + } + + private getModelMappingService(injector: Injector): ModelMappingService { + if (!this.options.withLegacyUndefineGeneration) { + return new ModelMappingService( + injector.get(OpenAPIService), + injector.get(OpenAPITypesGuard), + injector.get(TypesService), + injector.get(NameService) + ); + } + + class LegacyModelMappingService extends ModelMappingService { + protected isRequiredField(): boolean { + return false; + } + } + + return new LegacyModelMappingService( + injector.get(OpenAPIService), + injector.get(OpenAPITypesGuard), + injector.get(TypesService), + injector.get(NameService) + ); + } } diff --git a/src/models/InterfaceModel.ts b/src/models/InterfaceModel.ts index bc52919..20fd348 100644 --- a/src/models/InterfaceModel.ts +++ b/src/models/InterfaceModel.ts @@ -3,6 +3,7 @@ export interface IInterfacePropertyModel { name: string; dtoType: string; isNullable: boolean; + isRequired: boolean; } export interface IInterfaceModel { diff --git a/src/models/ObjectModel.ts b/src/models/ObjectModel.ts index feb0497..4c07631 100644 --- a/src/models/ObjectModel.ts +++ b/src/models/ObjectModel.ts @@ -2,8 +2,8 @@ import { IType } from './TypeModel'; export interface IObjectPropertyModel extends IType { name: string; - isNullable: boolean; isCollection: boolean; + isRequired: boolean; } export interface IObjectModel { diff --git a/src/options.ts b/src/options.ts index be7e40c..0295097 100644 --- a/src/options.ts +++ b/src/options.ts @@ -6,7 +6,8 @@ export const defaultOptions: IOptions = { utilsRelativePath: '', url: 'https://localhost:5001/swagger/v1/swagger.json', withRequestOptions: false, - unstrictId: false + unstrictId: false, + withLegacyUndefineGeneration: false }; export interface IOptions { @@ -19,6 +20,7 @@ export interface IOptions { withRequestOptions: boolean; unstrictId: boolean; utilsRelativePath: string; + withLegacyUndefineGeneration: boolean; } export const pathOptions = { diff --git a/src/services/ModelMappingService.ts b/src/services/ModelMappingService.ts index 9713166..cd330a4 100644 --- a/src/services/ModelMappingService.ts +++ b/src/services/ModelMappingService.ts @@ -43,15 +43,17 @@ export class ModelMappingService { } if (this.typesGuard.isObject(schema)) { + const idFieldName = 'id'; if (this.isIdentity(schema)) { identities.push({ name, isNullable: false, dtoType: this.nameService.getInterfaceName(name), property: { - ...this.typesService.getSimpleType(schema.properties['id'] as IOpenAPI3GuidSchema), + ...this.typesService.getSimpleType(schema.properties[idFieldName] as IOpenAPI3GuidSchema), + isRequired: this.isRequiredField(schema, idFieldName), isCollection: false, - name: 'id', + name: idFieldName, isNullable: true } }); @@ -72,6 +74,10 @@ export class ModelMappingService { }; } + protected isRequiredField(schema: IOpenAPI3ObjectSchema, fieldName: string): boolean { + return schema.required?.includes(fieldName) ?? false; + } + private toEnumModel(name: string, schema: IOpenAPI3EnumSchema): IEnumModel { return { name, @@ -127,28 +133,29 @@ export class ModelMappingService { if (!schema.properties) { return; } + Object.entries(schema.properties) .filter(([name]) => !IGNORE_PROPERTIES.includes(name)) - .forEach(([name, propertySchema]) => this.addProperty(model, name, propertySchema)); + .forEach(([name, propertySchema]) => this.addProperty(model, name, propertySchema, this.isRequiredField(schema, name))); model.properties = model.properties.sort(sortBy((z) => z.name)); } - private addProperty(model: IObjectModel, name: string, schema: OpenAPI3Schema): void { + private addProperty(model: IObjectModel, name: string, schema: OpenAPI3Schema, isRequired: boolean): void { if (this.typesGuard.isSimple(schema)) { - model.properties.push(this.getSimpleProperty(name, schema)); + model.properties.push(this.getSimpleProperty(name, schema, isRequired)); return; } let property: IObjectPropertyModel | undefined; if (this.typesGuard.isCollection(schema)) { if (this.typesGuard.isSimple(schema.items)) { - property = this.getSimpleProperty(name, schema.items); + property = this.getSimpleProperty(name, schema.items, isRequired); } else if (this.typesGuard.isReference(schema.items)) { - property = this.getReferenceProperty(name, schema.items); + property = this.getReferenceProperty(name, schema.items, isRequired); } if (this.typesGuard.isOneOf(schema.items)) { - property = this.getUnionReferenceProperty(name, first(schema.items.oneOf)); + property = this.getUnionReferenceProperty(name, first(schema.items.oneOf), isRequired); } if (property) { @@ -159,11 +166,11 @@ export class ModelMappingService { } if (this.typesGuard.isReference(schema)) { - property = this.getReferenceProperty(name, schema); + property = this.getReferenceProperty(name, schema, isRequired); } else if (this.typesGuard.isAllOf(schema)) { - property = this.getReferenceProperty(name, first(schema.allOf)); + property = this.getReferenceProperty(name, first(schema.allOf), isRequired); } else if (this.typesGuard.isOneOf(schema)) { - property = this.getUnionReferenceProperty(name, first(schema.oneOf)); + property = this.getUnionReferenceProperty(name, first(schema.oneOf), isRequired); } if (property) { @@ -207,16 +214,18 @@ export class ModelMappingService { }); } - private getSimpleProperty(name: string, schema: OpenAPI3SimpleSchema): IObjectPropertyModel { + private getSimpleProperty(name: string, schema: OpenAPI3SimpleSchema, isRequired: boolean): IObjectPropertyModel { return { ...this.typesService.getSimpleType(schema), name, isCollection: false, - isNullable: Boolean(schema.nullable) + isRequired + // TODO: its always return from getSimpleType + //isNullable: Boolean(schema.nullable) }; } - private getUnionReferenceProperty(name: string, schema: IOpenAPI3Reference): IObjectPropertyModel { + private getUnionReferenceProperty(name: string, schema: IOpenAPI3Reference, isRequired: boolean): IObjectPropertyModel { const schemaKey = this.openAPIService.getSchemaKey(schema); return { @@ -224,12 +233,13 @@ export class ModelMappingService { isCollection: false, name: name, isNullable: false, + isRequired, type: this.nameService.getUnionName(schemaKey), dtoType: this.nameService.getInterfaceName(this.nameService.getUnionName(schemaKey)) }; } - private getReferenceProperty(name: string, schema: IOpenAPI3Reference): IObjectPropertyModel { + private getReferenceProperty(name: string, schema: IOpenAPI3Reference, isRequired: boolean): IObjectPropertyModel { const schemaKey = this.openAPIService.getSchemaKey(schema); const refSchema = this.openAPIService.getRefSchema(schema); @@ -239,6 +249,7 @@ export class ModelMappingService { isCollection: false, name, isNullable: false, + isRequired, type: schemaKey, dtoType: schemaKey }; @@ -252,6 +263,7 @@ export class ModelMappingService { name, isNullable: true, type: schemaKey, + isRequired, dtoType: this.nameService.getInterfaceName(schemaKey) }; } @@ -259,7 +271,15 @@ export class ModelMappingService { private getInterfaces(identities: IIdentityModel[], objects: ObjectModel[]): InterfaceModel[] { const interfaces: IInterfaceModel[] = identities.map((z) => ({ name: this.nameService.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, + isRequired: z.property.isRequired, + isCollection: false, + isNullable: false + } + ] })); return interfaces.concat( @@ -272,7 +292,8 @@ export class ModelMappingService { name: x.name, dtoType: x.dtoType, isCollection: x.isCollection, - isNullable: x.isNullable + isNullable: x.isNullable, + isRequired: x.isRequired })) }; } else { @@ -282,7 +303,8 @@ export class ModelMappingService { name: x.name, dtoType: x.dtoType, isCollection: x.isCollection, - isNullable: x.isNullable + isNullable: x.isNullable, + isRequired: x.isRequired })) }; } diff --git a/src/swagger/v3/schemas/object-schema.ts b/src/swagger/v3/schemas/object-schema.ts index 2598f8f..81d83e2 100644 --- a/src/swagger/v3/schemas/object-schema.ts +++ b/src/swagger/v3/schemas/object-schema.ts @@ -3,6 +3,7 @@ import { OpenAPI3Schema } from './schema'; export interface IOpenAPI3ObjectSchema extends IOpenAPI3BaseSchema { type: 'object'; + required?: string[]; properties: { [key: string]: OpenAPI3Schema; }; diff --git a/swagger.json b/swagger.json index 4cf805e..6022826 100644 --- a/swagger.json +++ b/swagger.json @@ -723,6 +723,7 @@ }, "Product": { "type": "object", + "required": ["modifyDates", "categories","parentProduct", "status"], "properties": { "id": { "type": "string", @@ -786,6 +787,9 @@ ] } }, + "parentProduct": { + "$ref": "#/components/schemas/Product" + }, "status": { "allOf": [ {