Skip to content

Commit

Permalink
feat: add dataTypeMapping options to custom mapping for int64 and obj…
Browse files Browse the repository at this point in the history
…ect types
  • Loading branch information
shijistar committed May 4, 2024
1 parent 5bc9cda commit 28ed842
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 30 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ The `ServiceConfig` object has the following properties:
- `url`: _[required]_ The URL of the OpenAPI specification file. If you have a local file, you can use `input` instead of `url` to specify the file path. Or you can even use `spec` to provide the OpenAPI specification object directly. Either url, input, or spec is required.
- `apiBase`: The base path of all API endpoints. The service may be hosted on a subpath of the main domain, e.g., _/public-api/iam/v3_, then the apiBase is _/public-api_ in this case. If the original api path from the OpenAPI specification is acceptable, you don't need this field.
- `crossOrigin`: Whether to use the absolute api path when calling the service. This is useful when the service is hosted on a different domain and you need to set the `Access-Control-Allow-Origin` header. Default is `false`.
- `dataTypeMappings`: A map of some special data types to TypeScript types. The default is `{ int64: 'BigInt', object: 'Record<string, any>' }`.
- `output`: The output directory for the generated code. The default is `./src/api/{id}`.
- `httpClientFile`: Change the default path of `http-client.ts` file, so you can use your own http client. The default is `./http-client` under each service directory.
- `createApiInstance`: Whether to create an instance of each API class. The instance can only be created with an empty constructor, if you want to set different options for some api classes, you can set this to `false` and create the instance manually. Default is `true`.
Expand Down
73 changes: 52 additions & 21 deletions cli/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@ import type { GenerateApiParams } from 'swagger-typescript-api';
// @ts-ignore: TS7016 no declaration file
import type { SchemaParser } from 'swagger-typescript-api/src/schema-parser/schema-parser';
import { select } from '@inquirer/prompts';
import { cyan, gray, yellow } from 'colors/safe';
import { gray, magenta, yellow } from 'colors/safe';
import { camelCase } from 'lodash';
import { resolve } from 'path';
import prettier from 'prettier';
import signale from 'signale';
import { rootDir } from './paths';
import type { ServiceConfig } from './types';

const defaultDataMapping: Required<NonNullable<ServiceConfig['dataTypeMappings']>> = {
int64: 'BigInt',
object: 'Record<string, any>',
};
/**
* Generate code for a swagger service
* @param config the configuration for code generation
* @returns the generated result
*/
export async function generate(config: ServiceConfig) {
const { output = `./src/api/${config.id}`, ...otherConfig } = config;
const prettierConfig = await prettier.resolveConfig(process.cwd());
const { output = `./src/api/${config.id}`, dataTypeMappings, ...otherConfig } = config;
// pass empty to let prettier auto detect from process.cwd()
const prettierConfig = await prettier.resolveConfig('');
const mappings: typeof defaultDataMapping = {
...defaultDataMapping,
...dataTypeMappings,
};
const params: GenerateApiParams = {
modular: true,
templates: resolve(rootDir, 'templates'),
Expand All @@ -29,37 +40,48 @@ export async function generate(config: ServiceConfig) {
moduleNameFirstTag: true, // use Swagger tags to name service module files
sortRoutes: true,
createApiInstance: true,
patch: true,
output: output && resolve(output),
cleanOutput: true,
hooks: {
onFormatRouteName(routeInfo, templateRouteName) {
if (routeInfo.operationId) {
// Remove the trailing numbers of route name. If there are still duplicated names in the same module,
// an increasing number will be appended to the name and a warning message will also be printed.
return camelCase(routeInfo.operationId).replace(/\d+$/, '');
}
return templateRouteName;
},
},
primitiveTypeConstructs: (struct) => {
return {
...struct,
/*
type conversion:
int32 -> number
int64 -> string
int64 -> string | BigInt
*/
integer: {
int32: (_schema: SchemaDef, parser: SchemaParser) => {
return parser.config.Ts.Keyword.Number;
},
int64: (_schema: SchemaDef, parser: SchemaParser) => {
return parser.config.Ts.Keyword.String;
int64: (_schema: SchemaDef, _parser: SchemaParser) => {
return mappings.int64;
},
$default: (_schema: SchemaDef, parser: SchemaParser) => {
return parser.config.Ts.Keyword.Number;
},
},
object: {
$default: (_schema: SchemaDef, parser: SchemaParser) => {
return parser.config.Ts.Keyword.Any;
$default: (_schema: SchemaDef, _parser: SchemaParser) => {
return mappings.object;
},
},
};
},
prettier: {
...prettierConfig,
parser: 'typescript',
// parser: config.toJS ? 'babel' : 'typescript',
},
...otherConfig,
};
Expand All @@ -78,19 +100,28 @@ export const generateWithPrompt = async function (configList: ServiceConfig[]) {
name: `${item.id} ${gray(item.name ? `(${item.name})` : '')}`,
}));

const service = await select({
message: 'Which service do you want to generate for?',
// Prompt user to choose a service
return select({
message: magenta('Which service do you want to generate?'),
choices,
});

// eslint-disable-next-line no-console
console.info(`${cyan('Generating')} code of ${yellow(service)} service`);
const serviceConfig = configList.find((item) => item.id === service);
if (serviceConfig) {
return await generate(serviceConfig);
} else {
throw new Error(`Service ${service} not found`);
}
})
.then((serviceId) => {
const serviceConfig = configList.find((item) => item.id === serviceId);
if (serviceConfig) {
if (!serviceConfig.silent) {
// eslint-disable-next-line no-console
console.info(`Generating code for ${yellow(serviceId)} service`);
}
return generate(serviceConfig);
} else {
signale.fatal(`Service ${yellow(serviceId)} not found`);
return undefined;
}
})
.catch(() => {
// Silencing the cancellation error.
return undefined;
});
};

type SchemaDef = { type?: string; format?: string; name?: string; description?: string };
6 changes: 4 additions & 2 deletions cli/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ program
validateConfig(config);
}

let result: GenerateApiOutput;
let result: GenerateApiOutput | undefined;
if (Array.isArray(config)) {
result = await generateWithPrompt(config);
} else {
result = await generate(config);
}
signale.success('Code is generated to ', result.configuration.config.output);
if (result && !result.configuration.config.silent) {
signale.success('Code is generated to ', result.configuration.config.output);
}
});

program.parse(process.argv);
Expand Down
11 changes: 11 additions & 0 deletions cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,15 @@ export type ServiceConfig = GenerateApiParams & {
httpClientFile?: string;
/** Auto create an instance for each api class */
createApiInstance?: boolean;
/**
* Some custom data type mappings
*/
dataTypeMappings?: {
/**
* @default "number"
*/
int64?: 'number' | 'string' | 'BigInt';
/** @default "object" */
object?: 'object' | 'Record<string, any>';
};
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"colors": "^1.4.0",
"commander": "^12.0.0",
"cross-spawn": "^7.0.3",
"lodash": "^4.17.21",
"prettier": "^3.2.5",
"signale": "^1.4.0",
"swagger-typescript-api": "^13.0.3",
Expand All @@ -55,6 +56,7 @@
"@commitlint/config-conventional": "^19.2.2",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/cross-spawn": "^6.0.6",
"@types/lodash": "^4.17.1",
"@types/node": "^20.12.7",
"@types/signale": "^1.4.7",
"@typescript-eslint/eslint-plugin": "^7.8.0",
Expand Down
25 changes: 18 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions templates/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import { HttpClient, RequestParams, ContentType, HttpResponse } from "<%~ config
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
<% } %>
/**
<% if (config.moduleNameFirstTag) { %>
<%~ apiConfig.tags?.find(t => _.camelCase(t.name) === route.moduleName)?.description ?? route.moduleName %>
<% } else { %>
<%~ apiConfig.info?.title %>
<% } %>
*/
export class <%= apiClassName %>Class<SecurityDataType = unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {
<% if(config.singleHttpClient) { %>
http: HttpClient<SecurityDataType>;
Expand Down
3 changes: 3 additions & 0 deletions templates/data-contracts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @ts-nocheck: No need to check generated files

<%~ includeFile('@base/data-contracts.eta', it) %>

0 comments on commit 28ed842

Please sign in to comment.