diff --git a/README.md b/README.md index b851329a8..73ac4832f 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ $ openapi --help --postfixServices Service name postfix (default: "Service") --postfixModels Model name postfix --request Path to custom request file + --transformCase Transforms field names to specified case [camel, snake] (default: none) -h, --help display help for command Examples diff --git a/bin/index.js b/bin/index.js index 32f2fecbc..7105cc6b7 100755 --- a/bin/index.js +++ b/bin/index.js @@ -23,6 +23,7 @@ const params = program .option('--indent ', 'Indentation options [4, 2, tabs]', '4') .option('--postfixServices ', 'Service name postfix', 'Service') .option('--postfixModels ', 'Model name postfix') + .option('--transformCase ', 'Transforms field names to specified case [camel, snake]', 'none') .option('--request ', 'Path to custom request file') .parse(process.argv) .opts(); @@ -44,6 +45,7 @@ if (OpenAPI) { indent: params.indent, postfixServices: params.postfixServices, postfixModels: params.postfixModels, + transformCase: params.transformCase, request: params.request, }) .then(() => { diff --git a/src/Case.ts b/src/Case.ts new file mode 100644 index 000000000..c9f4e8309 --- /dev/null +++ b/src/Case.ts @@ -0,0 +1,54 @@ +import { Enum } from './client/interfaces/Enum'; +import { Model } from './client/interfaces/Model'; +import { OperationResponse } from './client/interfaces/OperationResponse'; +import { Service } from './client/interfaces/Service'; + +export enum Case { + NONE = 'none', + CAMEL = 'camel', + SNAKE = 'snake', +} +// Convert a string from snake case to camel case. +const toCamelCase = (str: string): string => { + return str.replace(/_([a-z0-9])/g, match => match[1].toUpperCase()); +}; + +// Convert a string from camel case or pascal case to snake case. +const toSnakeCase = (str: string): string => { + return str.replace(/([A-Z])/g, match => `_${match.toLowerCase()}`); +}; + +const transforms = { + [Case.CAMEL]: toCamelCase, + [Case.SNAKE]: toSnakeCase, +}; + +// A recursive function that looks at the models and their properties and +// converts each property name using the provided transform function. +export const convertModelCase = (model: T, type: Exclude): T => { + return { + ...model, + name: transforms[type](model.name), + link: model.link ? convertModelCase(model.link, type) : null, + enum: model.enum.map(modelEnum => convertEnumCase(modelEnum, type)), + enums: model.enums.map(property => convertModelCase(property, type)), + properties: model.properties.map(property => convertModelCase(property, type)), + }; +}; + +const convertEnumCase = (modelEnum: Enum, type: Exclude): Enum => { + return { + ...modelEnum, + name: transforms[type](modelEnum.name), + }; +}; + +export const convertServiceCase = (service: Service, type: Exclude): Service => { + return { + ...service, + operations: service.operations.map(op => ({ + ...op, + results: op.results.map(results => convertModelCase(results, type)), + })), + }; +}; diff --git a/src/index.ts b/src/index.ts index e63919085..a02110d43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { Case } from './Case'; import { HttpClient } from './HttpClient'; import { Indent } from './Indent'; import { parse as parseV2 } from './openApi/v2'; @@ -26,6 +27,7 @@ export type Options = { indent?: Indent; postfixServices?: string; postfixModels?: string; + transformCase?: Case; request?: string; write?: boolean; }; @@ -47,6 +49,7 @@ export type Options = { * @param indent Indentation options (4, 2 or tab) * @param postfixServices Service name postfix * @param postfixModels Model name postfix + * @param transformCase Transform case (camel, snake) * @param request Path to custom request file * @param write Write the files to disk (true or false) */ @@ -64,6 +67,7 @@ export const generate = async ({ indent = Indent.SPACE_4, postfixServices = 'Service', postfixModels = '', + transformCase = Case.NONE, request, write = true, }: Options): Promise => { @@ -94,6 +98,7 @@ export const generate = async ({ indent, postfixServices, postfixModels, + transformCase, clientName, request ); @@ -118,6 +123,7 @@ export const generate = async ({ indent, postfixServices, postfixModels, + transformCase, clientName, request ); diff --git a/src/utils/writeClient.spec.ts b/src/utils/writeClient.spec.ts index 3c06a95a5..884c77c6b 100644 --- a/src/utils/writeClient.spec.ts +++ b/src/utils/writeClient.spec.ts @@ -1,3 +1,4 @@ +import { Case } from '../Case'; import type { Client } from '../client/interfaces/Client'; import { HttpClient } from '../HttpClient'; import { Indent } from '../Indent'; @@ -49,7 +50,8 @@ describe('writeClient', () => { true, Indent.SPACE_4, 'Service', - 'AppClient' + 'AppClient', + Case.NONE ); expect(rmdir).toBeCalled(); diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index cea2f3d88..284d0a71e 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -1,6 +1,7 @@ import { resolve } from 'path'; import type { Client } from '../client/interfaces/Client'; +import { Case } from '../Case'; import type { HttpClient } from '../HttpClient'; import type { Indent } from '../Indent'; import { mkdir, rmdir } from './fileSystem'; @@ -30,6 +31,7 @@ import { writeClientServices } from './writeClientServices'; * @param indent Indentation options (4, 2 or tab) * @param postfixServices Service name postfix * @param postfixModels Model name postfix + * @param transformCase Transform model case (camel, snake) * @param clientName Custom client class name * @param request Path to custom request file */ @@ -47,6 +49,7 @@ export const writeClient = async ( indent: Indent, postfixServices: string, postfixModels: string, + transformCase: Case, clientName?: string, request?: string ): Promise => { @@ -78,6 +81,7 @@ export const writeClient = async ( useOptions, indent, postfixServices, + transformCase, clientName ); } @@ -91,7 +95,15 @@ export const writeClient = async ( if (exportModels) { await rmdir(outputPathModels); await mkdir(outputPathModels); - await writeClientModels(client.models, templates, outputPathModels, httpClient, useUnionTypes, indent); + await writeClientModels( + client.models, + templates, + outputPathModels, + httpClient, + useUnionTypes, + indent, + transformCase + ); } if (isDefined(clientName)) { diff --git a/src/utils/writeClientModels.spec.ts b/src/utils/writeClientModels.spec.ts index ee0f2b4f6..2dc1c36db 100644 --- a/src/utils/writeClientModels.spec.ts +++ b/src/utils/writeClientModels.spec.ts @@ -1,6 +1,7 @@ import { EOL } from 'os'; import { resolve } from 'path'; +import { Case } from '../Case'; import type { Model } from '../client/interfaces/Model'; import { HttpClient } from '../HttpClient'; import { Indent } from '../Indent'; @@ -52,7 +53,7 @@ describe('writeClientModels', () => { }, }; - await writeClientModels(models, templates, '/', HttpClient.FETCH, false, Indent.SPACE_4); + await writeClientModels(models, templates, '/', HttpClient.FETCH, false, Indent.SPACE_4, Case.NONE); expect(writeFile).toBeCalledWith(resolve('/', '/User.ts'), `model${EOL}`); }); diff --git a/src/utils/writeClientModels.ts b/src/utils/writeClientModels.ts index 997569b9f..7df02a645 100644 --- a/src/utils/writeClientModels.ts +++ b/src/utils/writeClientModels.ts @@ -1,6 +1,7 @@ import { resolve } from 'path'; import type { Model } from '../client/interfaces/Model'; +import { Case, convertModelCase } from '../Case'; import type { HttpClient } from '../HttpClient'; import type { Indent } from '../Indent'; import { writeFile } from './fileSystem'; @@ -16,6 +17,7 @@ import type { Templates } from './registerHandlebarTemplates'; * @param httpClient The selected httpClient (fetch, xhr, node or axios) * @param useUnionTypes Use union types instead of enums * @param indent Indentation options (4, 2 or tab) + * @param transformCase Transform model case (camel, snake) */ export const writeClientModels = async ( models: Model[], @@ -23,12 +25,14 @@ export const writeClientModels = async ( outputPath: string, httpClient: HttpClient, useUnionTypes: boolean, - indent: Indent + indent: Indent, + transformCase: Case ): Promise => { for (const model of models) { + const newModel = transformCase === Case.NONE ? model : convertModelCase(model, transformCase); const file = resolve(outputPath, `${model.name}.ts`); const templateResult = templates.exports.model({ - ...model, + ...newModel, httpClient, useUnionTypes, }); diff --git a/src/utils/writeClientServices.spec.ts b/src/utils/writeClientServices.spec.ts index f936d6609..67db4edf9 100644 --- a/src/utils/writeClientServices.spec.ts +++ b/src/utils/writeClientServices.spec.ts @@ -1,6 +1,7 @@ import { EOL } from 'os'; import { resolve } from 'path'; +import { Case } from '../Case'; import type { Service } from '../client/interfaces/Service'; import { HttpClient } from '../HttpClient'; import { Indent } from '../Indent'; @@ -40,7 +41,17 @@ describe('writeClientServices', () => { }, }; - await writeClientServices(services, templates, '/', HttpClient.FETCH, false, false, Indent.SPACE_4, 'Service'); + await writeClientServices( + services, + templates, + '/', + HttpClient.FETCH, + false, + false, + Indent.SPACE_4, + 'Service', + Case.NONE + ); expect(writeFile).toBeCalledWith(resolve('/', '/UserService.ts'), `service${EOL}`); }); diff --git a/src/utils/writeClientServices.ts b/src/utils/writeClientServices.ts index 2f95341d2..02f589c52 100644 --- a/src/utils/writeClientServices.ts +++ b/src/utils/writeClientServices.ts @@ -1,5 +1,6 @@ import { resolve } from 'path'; +import { Case, convertServiceCase } from '../Case'; import type { Service } from '../client/interfaces/Service'; import type { HttpClient } from '../HttpClient'; import type { Indent } from '../Indent'; @@ -19,6 +20,7 @@ import type { Templates } from './registerHandlebarTemplates'; * @param useOptions Use options or arguments functions * @param indent Indentation options (4, 2 or tab) * @param postfix Service name postfix + * @param transformCase Transform model case (camel, snake) * @param clientName Custom client class name */ export const writeClientServices = async ( @@ -30,12 +32,14 @@ export const writeClientServices = async ( useOptions: boolean, indent: Indent, postfix: string, + transformCase: Case, clientName?: string ): Promise => { for (const service of services) { + const newService = transformCase === Case.NONE ? service : convertServiceCase(service, transformCase); const file = resolve(outputPath, `${service.name}${postfix}.ts`); const templateResult = templates.exports.service({ - ...service, + ...newService, httpClient, useUnionTypes, useOptions,