diff --git a/.snapshot/all/models.ts b/.snapshot/all/models.ts index 732690a..81d74fe 100644 --- a/.snapshot/all/models.ts +++ b/.snapshot/all/models.ts @@ -12,6 +12,10 @@ export interface ICategory { name: $types.TypeOrUndefinedNullable; } +export interface IMyProduct.Core.Models.Feedback { + name: $types.TypeOrUndefinedNullable; +} + export interface IProduct { category: $types.TypeOrUndefinedNullable; colors: $types.TypeOrUndefined; @@ -57,6 +61,23 @@ export class Category { } } +export class MyProduct.Core.Models.Feedback { + public name: $types.TypeOrUndefinedNullable = undefined; + private __myProduct.Core.Models.Feedback!: string; + + public static toDTO(model: Partial): IMyProduct.Core.Models.Feedback { + return { + name: model.name, + }; + } + + public static fromDTO(dto: IMyProduct.Core.Models.Feedback): MyProduct.Core.Models.Feedback { + const model = new MyProduct.Core.Models.Feedback(); + model.name = dto.name; + return model; + } +} + export class Product { public category: $types.TypeOrUndefinedNullable = undefined; public colors: string[] = []; diff --git a/.snapshot/all/services.ts b/.snapshot/all/services.ts index 0e9bbeb..132179b 100644 --- a/.snapshot/all/services.ts +++ b/.snapshot/all/services.ts @@ -40,9 +40,10 @@ export class FeedbackService extends BaseHttpService { super(getBasePath('', '/api/feedback'), http); } - public postFeedback(): Observable { + public postFeedback(myProduct.Core.Models.Feedback: $models.IMyProduct.Core.Models.Feedback): Observable { return this.post( ``, + myProduct.Core.Models.Feedback, ); } } diff --git a/.snapshot/joinNamespace/models.ts b/.snapshot/joinNamespace/models.ts new file mode 100644 index 0000000..2f03798 --- /dev/null +++ b/.snapshot/joinNamespace/models.ts @@ -0,0 +1,117 @@ +import { Guid } from './Guid'; +import { toDateIn, toDateOut } from './date-converters'; +import type * as $types from './types'; + +export enum ProductStatus { + InStock = 0, + OutOfStock = -1, + UnderTheOrder = 1 +} + +export interface ICategory { + name: $types.TypeOrUndefinedNullable; +} + +export interface IMyProductCoreModelsFeedback { + name: $types.TypeOrUndefinedNullable; +} + +export interface IProduct { + category: $types.TypeOrUndefinedNullable; + colors: $types.TypeOrUndefined; + expireDate: $types.TypeOrUndefined; + externalId: $types.TypeOrUndefinedNullable; + id: $types.TypeOrUndefined; + modifyDates: $types.TypeOrUndefined; + name: $types.TypeOrUndefinedNullable; + status: $types.TypeOrUndefined; +} + +export interface IProductIdentityDTO { + id: $types.TypeOrUndefined; +} + +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 = undefined; + private __category!: string; + + public static toDTO(model: Partial): ICategory { + return { + name: model.name, + }; + } + + public static fromDTO(dto: ICategory): Category { + const model = new Category(); + model.name = dto.name; + return model; + } +} + +export class MyProductCoreModelsFeedback { + public name: $types.TypeOrUndefinedNullable = undefined; + private __myProductCoreModelsFeedback!: string; + + public static toDTO(model: Partial): IMyProductCoreModelsFeedback { + return { + name: model.name, + }; + } + + public static fromDTO(dto: IMyProductCoreModelsFeedback): MyProductCoreModelsFeedback { + const model = new MyProductCoreModelsFeedback(); + model.name = dto.name; + return model; + } +} + +export class Product { + public category: $types.TypeOrUndefinedNullable = 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; + private __product!: string; + + public static toDTO(model: Partial): IProduct { + return { + category: model.category ? Category.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, + status: model.status, + }; + } + + public static fromDTO(dto: IProduct): Product { + const model = new Product(); + model.category = dto.category ? Category.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; + } +} diff --git a/.snapshot/truncateNamespace/models.ts b/.snapshot/truncateNamespace/models.ts new file mode 100644 index 0000000..4ede77a --- /dev/null +++ b/.snapshot/truncateNamespace/models.ts @@ -0,0 +1,117 @@ +import { Guid } from './Guid'; +import { toDateIn, toDateOut } from './date-converters'; +import type * as $types from './types'; + +export enum ProductStatus { + InStock = 0, + OutOfStock = -1, + UnderTheOrder = 1 +} + +export interface ICategory { + name: $types.TypeOrUndefinedNullable; +} + +export interface IFeedback { + name: $types.TypeOrUndefinedNullable; +} + +export interface IProduct { + category: $types.TypeOrUndefinedNullable; + colors: $types.TypeOrUndefined; + expireDate: $types.TypeOrUndefined; + externalId: $types.TypeOrUndefinedNullable; + id: $types.TypeOrUndefined; + modifyDates: $types.TypeOrUndefined; + name: $types.TypeOrUndefinedNullable; + status: $types.TypeOrUndefined; +} + +export interface IProductIdentityDTO { + id: $types.TypeOrUndefined; +} + +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 = undefined; + private __category!: string; + + public static toDTO(model: Partial): ICategory { + return { + name: model.name, + }; + } + + public static fromDTO(dto: ICategory): Category { + const model = new Category(); + model.name = dto.name; + return model; + } +} + +export class Feedback { + public name: $types.TypeOrUndefinedNullable = undefined; + private __feedback!: string; + + public static toDTO(model: Partial): IFeedback { + return { + name: model.name, + }; + } + + public static fromDTO(dto: IFeedback): Feedback { + const model = new Feedback(); + model.name = dto.name; + return model; + } +} + +export class Product { + public category: $types.TypeOrUndefinedNullable = 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; + private __product!: string; + + public static toDTO(model: Partial): IProduct { + return { + category: model.category ? Category.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, + status: model.status, + }; + } + + public static fromDTO(dto: IProduct): Product { + const model = new Product(); + model.category = dto.category ? Category.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; + } +} diff --git a/.snapshot/withRequestOptions/services.ts b/.snapshot/withRequestOptions/services.ts index 746e15a..23d596d 100644 --- a/.snapshot/withRequestOptions/services.ts +++ b/.snapshot/withRequestOptions/services.ts @@ -42,9 +42,10 @@ export class FeedbackService extends BaseHttpService { super(getBasePath('', '/api/feedback'), http); } - public postFeedback(options?: $types.TypeOrUndefined): Observable { + public postFeedback(myProduct.Core.Models.Feedback: $models.IMyProduct.Core.Models.Feedback, options?: $types.TypeOrUndefined): Observable { return this.post( ``, + myProduct.Core.Models.Feedback, options, ); } diff --git a/README.md b/README.md index 7bb2212..0a6dce9 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,19 @@ 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 | | +| **truncateNamespace** | Generate schema object name with truncating namespace [more info](#fixNamespace) | boolean | false | +| **joinNamespace** | Join schema object name by dot [more info](#fixNamespace) | boolean | false | +| | ### Option details @@ -144,8 +145,78 @@ public static fromDTO(dto: IProduct): Product { } ``` +#### fixNamespace + +By default, GenGen generates model names with dots if any. + +Example: + +Object name in `Schemas` definition: + +`MyProduct.Core.Models.Product` + +Default behavior: + +```ts +export interface IMyProduct.Core.Models.Product {} + +export class MyProduct.Core.Models.Product { + private __myProduct.Core.Models.Product!: string; + + public static toDTO(model: Partial): IMyProduct.Core.Models.Product { + return {}; + } + + public static fromDTO(dto: IMyProduct.Core.Models.Product): MyProduct.Core.Models.Product { + const model = new MyProduct.Core.Models.Product(); + return model; + } +} +``` + +You can truncate or join namespace for the model name by using these options: + +1. With `truncateNamespace` option enabled: + +```ts +export interface IProduct {} + +export class Product { + private __product!: string; + + public static toDTO(model: Partial): IProduct { + return { + }; + } + + public static fromDTO(dto: IProduct): Product { + const model = new Product(); + return model; + } +} +``` + +2. With `joinNamespace` option enabled: + +```ts +export interface IMyProductCoreModelsProduct {} + +export class MyProductCoreModelsProduct { + private __myProductCoreModelsProduct!: string; + + public static toDTO(model: Partial): IMyProductCoreModelsProduct { + return {}; + } + + public static fromDTO(dto: IMyProductCoreModelsProduct): MyProductCoreModelsProduct { + const model = new MyProductCoreModelsProduct(); + return model; + } +} +``` + # License and copyright -Copyright (c) 2020-2023 Luxoft +Copyright (c) 2020-2024 Luxoft Licensed under the MIT license diff --git a/__tests__/services/ModelMappingService.spec.ts b/__tests__/services/ModelMappingService.spec.ts index bd98ccd..fedfa21 100644 --- a/__tests__/services/ModelMappingService.spec.ts +++ b/__tests__/services/ModelMappingService.spec.ts @@ -8,11 +8,11 @@ import { first } from '../../src/utils'; describe('ModelMappingService tests', () => { let service: ModelMappingService; + const guard = new OpenAPITypesGuard(); + const openAPIService = new MockOpenAPIService(guard); beforeEach(() => { - const guard = new OpenAPITypesGuard(); - const openAPIService = new MockOpenAPIService(guard); - service = new ModelMappingService(openAPIService, guard, new TypesService(guard, defaultOptions)); + service = new ModelMappingService(openAPIService, guard, new TypesService(guard, defaultOptions), defaultOptions); }); describe('toModelsContainer', () => { @@ -38,5 +38,55 @@ describe('ModelMappingService tests', () => { }); }); }); + + describe('toObjectModel', () => { + test('join namespace', () => { + service = service = new ModelMappingService( + openAPIService, + guard, + new TypesService(guard, defaultOptions), + { + ...defaultOptions, + joinNamespace: true, + } + ); + + const schemas: OpenAPI3SchemaContainer = { + "MyProduct.Core.Models.Product": { + "type": "object", + "properties": {} + } + }; + + const objectModel = first(service.toModelsContainer(schemas).objects); + expect(objectModel).toMatchObject({ + name: 'MyProductCoreModelsProduct' + }); + }); + + test('truncate namespace', () => { + service = service = new ModelMappingService( + openAPIService, + guard, + new TypesService(guard, defaultOptions), + { + ...defaultOptions, + truncateNamespace: true, + } + ); + + const schemas: OpenAPI3SchemaContainer = { + "MyProduct.Core.Models.Product": { + "type": "object", + "properties": {} + } + }; + + const objectModel = first(service.toModelsContainer(schemas).objects); + expect(objectModel).toMatchObject({ + name: 'Product' + }); + }); + }); }); }); diff --git a/e2e/e2e.ts b/e2e/e2e.ts index 8c4fbb4..125630b 100644 --- a/e2e/e2e.ts +++ b/e2e/e2e.ts @@ -11,6 +11,8 @@ 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/joinNamespace/models.ts', './.output/joinNamespace/models.ts', 'Models with JoinNamespace'); + snapshotter('./.snapshot/truncateNamespace/models.ts', './.output/truncateNamespace/models.ts', 'Models with TruncateNamespace'); } async function snapshotter(pathA: string, pathB: string, name: string) { diff --git a/package.json b/package.json index d3b5ad0..7b4ac8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@luxbss/gengen", - "version": "1.2.7", + "version": "1.2.8", "description": "Tool for generating models and Angular services based on OpenAPIs and Swagger's JSON", "bin": { "gengen": "./bin/index.js" @@ -12,8 +12,10 @@ "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:joinNamespace": "node ./bin/index.js g --all --file=./swagger.json --output=./.output/joinNamespace --joinNamespace", + "g:truncateNamespace": "node ./bin/index.js g --all --file=./swagger.json --output=./.output/truncateNamespace --truncateNamespace", "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:joinNamespace && npm run g:truncateNamespace && ts-node ./e2e/e2e.ts", "test": "jest", "test:w": "jest --watch", "coverage": "jest --coverage", diff --git a/src/bin.ts b/src/bin.ts index 7bdc0bb..04f74be 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -32,6 +32,8 @@ program .option('--all') .option('--withRequestOptions') .option('--unstrictId') + .option('--joinNamespace') + .option('--truncateNamespace') .description('Generates models and services') .action(async (params) => { const options = getOptions(params); diff --git a/src/gengen/GenGenCodeGenInjector.ts b/src/gengen/GenGenCodeGenInjector.ts index 8ca312e..74a506e 100644 --- a/src/gengen/GenGenCodeGenInjector.ts +++ b/src/gengen/GenGenCodeGenInjector.ts @@ -85,7 +85,7 @@ export class GenGenCodeGenInjector { ) .provide( ModelMappingService, - (x) => new ModelMappingService(x.get(OpenAPIService), x.get(OpenAPITypesGuard), x.get(TypesService)) + (x) => new ModelMappingService(x.get(OpenAPIService), x.get(OpenAPITypesGuard), x.get(TypesService), this.options) ) .provide(TypesService, (x) => new TypesService(x.get(OpenAPITypesGuard), this.options)) .provide(OpenAPIService, (x) => new OpenAPIService(this.spec, x.get(OpenAPITypesGuard))) diff --git a/src/options.ts b/src/options.ts index be7e40c..57ab2e3 100644 --- a/src/options.ts +++ b/src/options.ts @@ -6,7 +6,9 @@ export const defaultOptions: IOptions = { utilsRelativePath: '', url: 'https://localhost:5001/swagger/v1/swagger.json', withRequestOptions: false, - unstrictId: false + unstrictId: false, + joinNamespace: false, + truncateNamespace: false, }; export interface IOptions { @@ -19,6 +21,8 @@ export interface IOptions { withRequestOptions: boolean; unstrictId: boolean; utilsRelativePath: string; + joinNamespace: boolean; + truncateNamespace: boolean; } export const pathOptions = { diff --git a/src/services/ModelMappingService.ts b/src/services/ModelMappingService.ts index df89ea7..9893960 100644 --- a/src/services/ModelMappingService.ts +++ b/src/services/ModelMappingService.ts @@ -13,6 +13,7 @@ import { IOpenAPI3ObjectSchema } from '../swagger/v3/schemas/object-schema'; import { OpenAPI3Schema, OpenAPI3SchemaContainer, OpenAPI3SimpleSchema } from '../swagger/v3/schemas/schema'; import { first, sortBy } from '../utils'; import { TypesService } from './TypesService'; +import { IOptions } from '../options'; const IGNORE_PROPERTIES = ['startRow', 'rowCount']; @@ -20,7 +21,8 @@ export class ModelMappingService { constructor( private readonly openAPIService: OpenAPIService, private readonly typesGuard: OpenAPITypesGuard, - private readonly typesService: TypesService + private readonly typesService: TypesService, + private readonly settings: IOptions ) {} public toModelsContainer(schemas: OpenAPI3SchemaContainer): IModelsContainer { @@ -29,6 +31,8 @@ export class ModelMappingService { const objects: IObjectModel[] = []; Object.entries(schemas).forEach(([name, schema]) => { + name = this.transformName(name, this.settings.joinNamespace, this.settings.truncateNamespace); + if (this.typesGuard.isEnum(schema)) { enums.push(this.toEnumModel(name, schema)); return; @@ -186,4 +190,30 @@ export class ModelMappingService { } return schema.properties && Object.keys(schema.properties)?.length === 1 && this.typesGuard.isGuid(schema.properties['id']); } + + private transformName(name: string, joinNamespace: boolean, truncateNamespace: boolean): string { + const namespaceSeparator = '.'; + + if (!joinNamespace && !truncateNamespace) { + return name; + } + + if (joinNamespace) { + return this.joinBy(name, namespaceSeparator); + } + + if (truncateNamespace) { + return this.truncateNamespace(name, namespaceSeparator); + } + + return name; + } + + private joinBy(value: string, separator: string, replaceWith = ''): string { + return value.replaceAll(separator, replaceWith); + } + + private truncateNamespace(value: string, separator: string): string { + return value.split(separator).slice(-1)[0]; + } } diff --git a/swagger.json b/swagger.json index 718f33a..f5b79fb 100644 --- a/swagger.json +++ b/swagger.json @@ -653,6 +653,33 @@ } } } + }, + "/api/feedback": { + "post": { + "tags": [ + "feedback" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyProduct.Core.Models.Feedback" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Server Error" + } + } + } } }, "components": { @@ -739,6 +766,16 @@ } }, "additionalProperties": false + }, + "MyProduct.Core.Models.Feedback": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false } } } diff --git a/tsconfig.json b/tsconfig.json index 0956ddf..5bfdec5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "outDir": "./lib", "declaration": true, "lib": [ - "es2019", + "es2021", "dom" ] },