From 74036e2d7287476ce5acd0813f7739fdb17290ce Mon Sep 17 00:00:00 2001 From: Rob Dodson Date: Sun, 2 Apr 2023 19:24:13 -0700 Subject: [PATCH 1/6] Fixes #1252. Adds transformModelCase flag. --- bin/index.js | 2 ++ src/Case.ts | 38 +++++++++++++++++++++++++++++ src/index.ts | 6 +++++ src/utils/writeClient.spec.ts | 4 ++- src/utils/writeClient.ts | 13 +++++++++- src/utils/writeClientModels.spec.ts | 3 ++- src/utils/writeClientModels.ts | 8 ++++-- 7 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/Case.ts diff --git a/bin/index.js b/bin/index.js index 32f2fecbc..b54c9e82d 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('--transformModelCase ', 'Transform model 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, + transformModelCase: params.transformModelCase, request: params.request, }) .then(() => { diff --git a/src/Case.ts b/src/Case.ts new file mode 100644 index 000000000..e98def5c7 --- /dev/null +++ b/src/Case.ts @@ -0,0 +1,38 @@ +import { Model } from './client/interfaces/Model'; + +export enum Case { + NONE = 'none', + CAMEL = 'camel', + SNAKE = 'snake', +} +// Convert a string from snake case or pascal case to camel case. +const toCamelCase = (str: string): string => { + return str.replace(/_([a-z])/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 convertModelNames = (model: Model, type: Exclude): Model => { + if (!model.properties.length) { + return { + ...model, + name: transforms[type](model.name), + }; + } + return { + ...model, + properties: model.properties.map(property => { + return convertModelNames(property, type); + }), + }; +}; diff --git a/src/index.ts b/src/index.ts index e63919085..cf176eada 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; + transformModelCase?: 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 transformModelCase Transform model 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 = '', + transformModelCase = Case.NONE, request, write = true, }: Options): Promise => { @@ -94,6 +98,7 @@ export const generate = async ({ indent, postfixServices, postfixModels, + transformModelCase, clientName, request ); @@ -118,6 +123,7 @@ export const generate = async ({ indent, postfixServices, postfixModels, + transformModelCase, 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..2e046468c 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 transformModelCase 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, + transformModelCase: Case, clientName?: string, request?: string ): Promise => { @@ -91,7 +94,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, + transformModelCase + ); } 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..28f399c29 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, convertModelNames } 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 transformModelCase 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, + transformModelCase: Case ): Promise => { for (const model of models) { + const newModel = transformModelCase === Case.NONE ? model : convertModelNames(model, transformModelCase); const file = resolve(outputPath, `${model.name}.ts`); const templateResult = templates.exports.model({ - ...model, + ...newModel, httpClient, useUnionTypes, }); From 483f67231826ed2d15f02cd05cbde655861bbe67 Mon Sep 17 00:00:00 2001 From: Oskar Asplin Date: Sat, 20 May 2023 06:32:47 +0200 Subject: [PATCH 2/6] Fix case conversion for all fields --- src/Case.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Case.ts b/src/Case.ts index e98def5c7..1832a439a 100644 --- a/src/Case.ts +++ b/src/Case.ts @@ -1,3 +1,4 @@ +import { Enum } from './client/interfaces/Enum'; import { Model } from './client/interfaces/Model'; export enum Case { @@ -23,16 +24,19 @@ const transforms = { // A recursive function that looks at the models and their properties and // converts each property name using the provided transform function. export const convertModelNames = (model: Model, type: Exclude): Model => { - if (!model.properties.length) { - return { - ...model, - name: transforms[type](model.name), - }; - } return { ...model, - properties: model.properties.map(property => { - return convertModelNames(property, type); - }), + name: transforms[type](model.name), + link: model.link ? convertModelNames(model.link, type) : null, + enum: model.enum.map(modelEnum => convertEnumName(modelEnum, type)), + enums: model.enums.map(property => convertModelNames(property, type)), + properties: model.properties.map(property => convertModelNames(property, type)), + }; +}; + +const convertEnumName = (modelEnum: Enum, type: Exclude): Enum => { + return { + ...modelEnum, + name: transforms[type](modelEnum.name), }; }; From 07edda95121b205f308a1cb2827d7478f154727d Mon Sep 17 00:00:00 2001 From: Oskar Asplin Date: Tue, 30 May 2023 12:33:24 +0200 Subject: [PATCH 3/6] Rename transformModelCase to transformCase and add to Readme --- README.md | 1 + bin/index.js | 4 ++-- src/index.ts | 10 +++++----- src/utils/writeClient.ts | 6 +++--- src/utils/writeClientModels.ts | 6 +++--- 5 files changed, 14 insertions(+), 13 deletions(-) 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 b54c9e82d..7105cc6b7 100755 --- a/bin/index.js +++ b/bin/index.js @@ -23,7 +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('--transformModelCase ', 'Transform model case [camel, snake]', 'none') + .option('--transformCase ', 'Transforms field names to specified case [camel, snake]', 'none') .option('--request ', 'Path to custom request file') .parse(process.argv) .opts(); @@ -45,7 +45,7 @@ if (OpenAPI) { indent: params.indent, postfixServices: params.postfixServices, postfixModels: params.postfixModels, - transformModelCase: params.transformModelCase, + transformCase: params.transformCase, request: params.request, }) .then(() => { diff --git a/src/index.ts b/src/index.ts index cf176eada..a02110d43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ export type Options = { indent?: Indent; postfixServices?: string; postfixModels?: string; - transformModelCase?: Case; + transformCase?: Case; request?: string; write?: boolean; }; @@ -49,7 +49,7 @@ export type Options = { * @param indent Indentation options (4, 2 or tab) * @param postfixServices Service name postfix * @param postfixModels Model name postfix - * @param transformModelCase Transform model case (camel, snake) + * @param transformCase Transform case (camel, snake) * @param request Path to custom request file * @param write Write the files to disk (true or false) */ @@ -67,7 +67,7 @@ export const generate = async ({ indent = Indent.SPACE_4, postfixServices = 'Service', postfixModels = '', - transformModelCase = Case.NONE, + transformCase = Case.NONE, request, write = true, }: Options): Promise => { @@ -98,7 +98,7 @@ export const generate = async ({ indent, postfixServices, postfixModels, - transformModelCase, + transformCase, clientName, request ); @@ -123,7 +123,7 @@ export const generate = async ({ indent, postfixServices, postfixModels, - transformModelCase, + transformCase, clientName, request ); diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index 2e046468c..bcdfcde42 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -31,7 +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 transformModelCase Transform model case (camel, snake) + * @param transformCase Transform model case (camel, snake) * @param clientName Custom client class name * @param request Path to custom request file */ @@ -49,7 +49,7 @@ export const writeClient = async ( indent: Indent, postfixServices: string, postfixModels: string, - transformModelCase: Case, + transformCase: Case, clientName?: string, request?: string ): Promise => { @@ -101,7 +101,7 @@ export const writeClient = async ( httpClient, useUnionTypes, indent, - transformModelCase + transformCase ); } diff --git a/src/utils/writeClientModels.ts b/src/utils/writeClientModels.ts index 28f399c29..d3af0be89 100644 --- a/src/utils/writeClientModels.ts +++ b/src/utils/writeClientModels.ts @@ -17,7 +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 transformModelCase Transform model case (camel, snake) + * @param transformCase Transform model case (camel, snake) */ export const writeClientModels = async ( models: Model[], @@ -26,10 +26,10 @@ export const writeClientModels = async ( httpClient: HttpClient, useUnionTypes: boolean, indent: Indent, - transformModelCase: Case + transformCase: Case ): Promise => { for (const model of models) { - const newModel = transformModelCase === Case.NONE ? model : convertModelNames(model, transformModelCase); + const newModel = transformCase === Case.NONE ? model : convertModelNames(model, transformCase); const file = resolve(outputPath, `${model.name}.ts`); const templateResult = templates.exports.model({ ...newModel, From 262a1bcf705bdd3847644301a1b6877115b17881 Mon Sep 17 00:00:00 2001 From: Oskar Asplin Date: Tue, 30 May 2023 14:40:16 +0200 Subject: [PATCH 4/6] Fix case conversion for service operation response body --- src/Case.ts | 16 ++++++++++++++-- src/utils/writeClient.ts | 1 + src/utils/writeClientServices.spec.ts | 13 ++++++++++++- src/utils/writeClientServices.ts | 6 +++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Case.ts b/src/Case.ts index 1832a439a..da648c3ad 100644 --- a/src/Case.ts +++ b/src/Case.ts @@ -1,12 +1,14 @@ 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 or pascal case to camel case. +// Convert a string from snake case to camel case. const toCamelCase = (str: string): string => { return str.replace(/_([a-z])/g, match => match[1].toUpperCase()); }; @@ -23,7 +25,7 @@ const transforms = { // A recursive function that looks at the models and their properties and // converts each property name using the provided transform function. -export const convertModelNames = (model: Model, type: Exclude): Model => { +export const convertModelNames = (model: T, type: Exclude): T => { return { ...model, name: transforms[type](model.name), @@ -40,3 +42,13 @@ const convertEnumName = (modelEnum: Enum, type: Exclude): Enum 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 => convertModelNames(results, type)), + })), + }; +}; diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index bcdfcde42..284d0a71e 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -81,6 +81,7 @@ export const writeClient = async ( useOptions, indent, postfixServices, + transformCase, clientName ); } 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, From d2fc59fbf5714a18feea2d4638967a37f3458432 Mon Sep 17 00:00:00 2001 From: Rob Dodson Date: Fri, 28 Jul 2023 19:19:45 -0700 Subject: [PATCH 5/6] Rename convert methods to be consistent --- src/Case.ts | 14 +++++++------- src/utils/writeClientModels.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Case.ts b/src/Case.ts index da648c3ad..85477f8f7 100644 --- a/src/Case.ts +++ b/src/Case.ts @@ -25,18 +25,18 @@ const transforms = { // A recursive function that looks at the models and their properties and // converts each property name using the provided transform function. -export const convertModelNames = (model: T, type: Exclude): T => { +export const convertModelCase = (model: T, type: Exclude): T => { return { ...model, name: transforms[type](model.name), - link: model.link ? convertModelNames(model.link, type) : null, - enum: model.enum.map(modelEnum => convertEnumName(modelEnum, type)), - enums: model.enums.map(property => convertModelNames(property, type)), - properties: model.properties.map(property => convertModelNames(property, type)), + 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 convertEnumName = (modelEnum: Enum, type: Exclude): Enum => { +const convertEnumCase = (modelEnum: Enum, type: Exclude): Enum => { return { ...modelEnum, name: transforms[type](modelEnum.name), @@ -48,7 +48,7 @@ export const convertServiceCase = (service: Service, type: Exclude ({ ...op, - results: op.results.map(results => convertModelNames(results, type)), + results: op.results.map(results => convertModelCase(results, type)), })), }; }; diff --git a/src/utils/writeClientModels.ts b/src/utils/writeClientModels.ts index d3af0be89..7df02a645 100644 --- a/src/utils/writeClientModels.ts +++ b/src/utils/writeClientModels.ts @@ -1,7 +1,7 @@ import { resolve } from 'path'; import type { Model } from '../client/interfaces/Model'; -import { Case, convertModelNames } from '../Case'; +import { Case, convertModelCase } from '../Case'; import type { HttpClient } from '../HttpClient'; import type { Indent } from '../Indent'; import { writeFile } from './fileSystem'; @@ -29,7 +29,7 @@ export const writeClientModels = async ( transformCase: Case ): Promise => { for (const model of models) { - const newModel = transformCase === Case.NONE ? model : convertModelNames(model, transformCase); + const newModel = transformCase === Case.NONE ? model : convertModelCase(model, transformCase); const file = resolve(outputPath, `${model.name}.ts`); const templateResult = templates.exports.model({ ...newModel, From 67b5def2d8ac3da7b291caa82c66d522a0b57d82 Mon Sep 17 00:00:00 2001 From: Rob Dodson Date: Wed, 2 Aug 2023 12:03:19 -0700 Subject: [PATCH 6/6] Handle numbers in camel case strings --- src/Case.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Case.ts b/src/Case.ts index 85477f8f7..c9f4e8309 100644 --- a/src/Case.ts +++ b/src/Case.ts @@ -10,7 +10,7 @@ export enum Case { } // Convert a string from snake case to camel case. const toCamelCase = (str: string): string => { - return str.replace(/_([a-z])/g, match => match[1].toUpperCase()); + return str.replace(/_([a-z0-9])/g, match => match[1].toUpperCase()); }; // Convert a string from camel case or pascal case to snake case.