Skip to content

Commit 4e05c82

Browse files
committed
feat: 解析枚举描述生成枚举标签
1 parent 9f03feb commit 4e05c82

File tree

7 files changed

+262
-5
lines changed

7 files changed

+262
-5
lines changed

README-en_US.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ $ openapi --help
204204
--isTranslateToEnglishTag <boolean> translate chinese tag name to english tag name (default: false)
205205
--isOnlyGenTypeScriptType <boolean> only generate typescript type (default: false)
206206
--isCamelCase <boolean> camelCase naming of controller files and request client (default: true)
207+
--isSupportParseEnumDesc <boolean> parse enum description to generate enum label (default: false)
207208
-h, --help display help for command
208209
```
209210

@@ -240,6 +241,7 @@ openapi -i ./spec.json -o ./apis
240241
| isTranslateToEnglishTag | no | boolean | false | translate chinese tag name to english tag name |
241242
| isOnlyGenTypeScriptType | no | boolean | false | only generate typescript type |
242243
| isCamelCase | no | boolean | true | camelCase naming of controller files and request client |
244+
| isSupportParseEnumDesc | no | boolean | false | parse enum description to generate enum label, format example: `UserRole:User(Normal User)=0,Agent(Agent)=1,Admin(Administrator)=2` |
243245
| hook | no | [Custom Hook](#Custom-Hook) | - | custom hook |
244246

245247
## Custom Hook

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ $ openapi --help
204204
--isTranslateToEnglishTag <boolean> translate chinese tag name to english tag name (default: false)
205205
--isOnlyGenTypeScriptType <boolean> only generate typescript type (default: false)
206206
--isCamelCase <boolean> camelCase naming of controller files and request client (default: true)
207+
--isSupportParseEnumDesc <boolean> parse enum description to generate enum label (default: false)
208+
--includeDescEnums <(string|RegExp)[]> generate code from include enums
209+
--excludeDescEnums <(string|RegExp)[]> generate code from exclude enums, if includeDescEnums is not empty, this value is invalid
207210
-h, --help display help for command
208211
```
209212

@@ -240,6 +243,7 @@ openapi --i ./spec.json --o ./apis
240243
| isTranslateToEnglishTag || boolean | false | 将中文 tag 名称翻译成英文 tag 名称 |
241244
| isOnlyGenTypeScriptType || boolean | false | 仅生成 typescript 类型 |
242245
| isCamelCase || boolean | true | 小驼峰命名文件和请求函数 |
246+
| isSupportParseEnumDesc || boolean | false | 解析枚举描述生成枚举标签,格式参考:`系统用户角色:User(普通用户)=0,Agent(经纪人)=1,Admin(管理员)=2` |
243247
| hook || [Custom Hook](#Custom-Hook) | - | 自定义 hook |
244248

245249
## 自定义 Hook

src/generator/serviceGenarator.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
isReferenceObject,
9191
isSchemaObject,
9292
markAllowedSchema,
93+
parseDescriptionEnum,
9394
replaceDot,
9495
resolveFunctionName,
9596
resolveRefs,
@@ -1122,7 +1123,16 @@ export default class ServiceGenerator {
11221123
let enumLabelTypeStr = '';
11231124

11241125
if (numberEnum.includes(schemaObject.type) || isAllNumber(enumArray)) {
1125-
enumStr = `{${map(enumArray, (value) => `"NUMBER_${value}"=${Number(value)}`).join(',')}}`;
1126+
if (this.config.isSupportParseEnumDesc && schemaObject.description) {
1127+
const enumMap = parseDescriptionEnum(schemaObject.description);
1128+
enumStr = `{${map(enumArray, (value) => {
1129+
const enumLabel = enumMap.get(Number(value));
1130+
return `${enumLabel}=${Number(value)}`;
1131+
}).join(',')}}`;
1132+
log('EnumDesc enumStr', enumStr);
1133+
} else {
1134+
enumStr = `{${map(enumArray, (value) => `"NUMBER_${value}"=${Number(value)}`).join(',')}}`;
1135+
}
11261136
} else if (isAllNumeric(enumArray)) {
11271137
enumStr = `{${map(enumArray, (value) => `"STRING_NUMBER_${value}"="${value}"`).join(',')}}`;
11281138
} else {
@@ -1153,7 +1163,15 @@ export default class ServiceGenerator {
11531163
}).join(',')}}`;
11541164
} else {
11551165
if (numberEnum.includes(schemaObject.type) || isAllNumber(enumArray)) {
1156-
enumLabelTypeStr = `{${map(enumArray, (value) => `"NUMBER_${value}":${Number(value)}`).join(',')}}`;
1166+
if (this.config.isSupportParseEnumDesc && schemaObject.description) {
1167+
const enumMap = parseDescriptionEnum(schemaObject.description);
1168+
enumLabelTypeStr = `{${map(enumArray, (value) => {
1169+
const enumLabel = enumMap.get(Number(value));
1170+
return `"${enumLabel}":${Number(value)}`;
1171+
}).join(',')}}`;
1172+
} else {
1173+
enumLabelTypeStr = `{${map(enumArray, (value) => `"NUMBER_${value}":${Number(value)}`).join(',')}}`;
1174+
}
11571175
} else if (isAllNumeric(enumArray)) {
11581176
enumLabelTypeStr = `{${map(enumArray, (value) => `"STRING_NUMBER_${value}":"${value}"`).join(',')}}`;
11591177
} else {

src/generator/util.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,3 +494,58 @@ export function isAllNumber(arr) {
494494
export function capitalizeFirstLetter(str: string) {
495495
return str.replace(/^[a-z]/, (match) => match.toUpperCase());
496496
}
497+
498+
// 解析 description 中的枚举翻译
499+
export const parseDescriptionEnum = (
500+
description: string
501+
): Map<number, string> => {
502+
const enumMap = new Map<number, string>();
503+
if (!description) return enumMap;
504+
505+
// 首先处理可能的总体描述,例如 "系统用户角色:User=0,..."
506+
let descToProcess = description;
507+
const mainDescriptionMatch = description.match(/^([^:]+):(.*)/);
508+
if (mainDescriptionMatch) {
509+
// 如果有总体描述(如 "系统用户角色:"),只处理冒号后面的部分
510+
descToProcess = mainDescriptionMatch[2];
511+
}
512+
513+
// 匹配形如 "User(普通用户)=0" 或 "User=0" 的模式
514+
const enumPattern = /([^():,=]+)(?:\(([^)]+)\))?=(\d+)/g;
515+
let match: RegExpExecArray | null;
516+
517+
while ((match = enumPattern.exec(descToProcess)) !== null) {
518+
const name = match[1] ? match[1].trim() : '';
519+
const valueStr = match[3] ? match[3].trim() : '';
520+
521+
if (valueStr && !isNaN(Number(valueStr))) {
522+
// 统一使用英文key(如User)
523+
enumMap.set(Number(valueStr), name);
524+
}
525+
}
526+
527+
// 如果没有匹配到任何枚举,尝试使用简单的分割方法作为后备
528+
if (enumMap.size === 0) {
529+
const pairs = descToProcess.split(',');
530+
pairs.forEach((pair) => {
531+
const parts = pair.split('=');
532+
if (parts.length === 2) {
533+
let label = parts[0].trim();
534+
const value = parts[1].trim();
535+
536+
// 处理可能带有括号的情况
537+
const bracketMatch = label.match(/([^(]+)\(([^)]+)\)/);
538+
if (bracketMatch) {
539+
// 只使用括号前的英文key
540+
label = bracketMatch[1].trim();
541+
}
542+
543+
if (label && value && !isNaN(Number(value))) {
544+
enumMap.set(Number(value), label);
545+
}
546+
}
547+
});
548+
}
549+
550+
return enumMap;
551+
};

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ export type GenerateServiceProps = {
131131
* 模板文件、请求函数采用小驼峰命名
132132
*/
133133
isCamelCase?: boolean;
134+
/**
135+
* 是否使用 description 中的枚举定义
136+
*/
137+
isSupportParseEnumDesc?: boolean;
134138
/**
135139
* 命名空间名称,默认为API,不需要关注
136140
*/
@@ -274,6 +278,7 @@ export async function generateService({
274278
nullable: false,
275279
isOnlyGenTypeScriptType: false,
276280
isCamelCase: true,
281+
isSupportParseEnumDesc: false,
277282
...rest,
278283
},
279284
openAPI
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "DeepSprite.House",
5+
"description": "DeepSprite.House",
6+
"version": "v9.0.2"
7+
},
8+
"servers": [
9+
{
10+
"url": "http://localhost:8010",
11+
"description": ""
12+
}
13+
],
14+
"paths": {
15+
"/api/sys/user/get-user-info": {
16+
"get": {
17+
"tags": ["user"],
18+
"summary": "获取用户信息",
19+
"operationId": "api-sys-user-get-user-info-get",
20+
"parameters": [
21+
{
22+
"name": "userId",
23+
"in": "query",
24+
"description": "",
25+
"schema": {
26+
"type": "integer",
27+
"format": "int64"
28+
}
29+
}
30+
],
31+
"responses": {
32+
"200": {
33+
"description": "OK",
34+
"content": {
35+
"text/plain": {
36+
"schema": {
37+
"$ref": "#/components/schemas/ResultOutputUserInfoOutput"
38+
}
39+
},
40+
"application/json": {
41+
"schema": {
42+
"$ref": "#/components/schemas/ResultOutputUserInfoOutput"
43+
}
44+
},
45+
"text/json": {
46+
"schema": {
47+
"$ref": "#/components/schemas/ResultOutputUserInfoOutput"
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
},
56+
"components": {
57+
"schemas": {
58+
"ResultOutputString": {
59+
"type": "object",
60+
"properties": {
61+
"success": {
62+
"type": "boolean",
63+
"description": "是否成功标记"
64+
},
65+
"code": {
66+
"type": "string",
67+
"description": "编码",
68+
"nullable": true
69+
},
70+
"msg": {
71+
"type": "string",
72+
"description": "消息",
73+
"nullable": true
74+
},
75+
"data": {
76+
"type": "string",
77+
"description": "数据",
78+
"nullable": true
79+
}
80+
},
81+
"additionalProperties": false,
82+
"description": "结果输出"
83+
},
84+
"ResultOutputUserInfoOutput": {
85+
"type": "object",
86+
"properties": {
87+
"success": {
88+
"type": "boolean",
89+
"description": "是否成功标记"
90+
},
91+
"code": {
92+
"type": "string",
93+
"description": "编码",
94+
"nullable": true
95+
},
96+
"msg": {
97+
"type": "string",
98+
"description": "消息",
99+
"nullable": true
100+
},
101+
"data": {
102+
"$ref": "#/components/schemas/UserInfoOutput"
103+
}
104+
},
105+
"additionalProperties": false,
106+
"description": "结果输出"
107+
},
108+
"SortOrder": {
109+
"enum": [0, 1],
110+
"type": "integer",
111+
"description": "排序方式:Asc=0,Desc=1",
112+
"format": "int32"
113+
},
114+
"SysUserRole": {
115+
"enum": [0, 1, 2],
116+
"type": "integer",
117+
"description": "系统用户角色:User(普通用户)=0,Agent(经纪人)=1,Admin(管理员)=2",
118+
"format": "int32"
119+
},
120+
"UserInfoOutput": {
121+
"type": "object",
122+
"properties": {
123+
"nickName": {
124+
"type": "string",
125+
"description": "昵称",
126+
"nullable": true
127+
},
128+
"avatar": {
129+
"type": "string",
130+
"description": "头像",
131+
"nullable": true
132+
},
133+
"phone": {
134+
"type": "string",
135+
"description": "手机号",
136+
"nullable": true
137+
},
138+
"role": {
139+
"$ref": "#/components/schemas/SysUserRole"
140+
}
141+
},
142+
"additionalProperties": false
143+
}
144+
},
145+
"securitySchemes": {
146+
"Bearer": {
147+
"type": "apiKey",
148+
"description": "Value: Bearer {token}",
149+
"name": "Authorization",
150+
"in": "header"
151+
}
152+
}
153+
},
154+
"security": [
155+
{
156+
"Bearer": []
157+
}
158+
],
159+
"tags": [
160+
{
161+
"name": "user",
162+
"description": "用户服务"
163+
}
164+
]
165+
}

test/test.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ const gen = async () => {
6161
// 自定义类型名
6262
customTypeName: (data) => {
6363
const { operationId } = data;
64-
const funName = operationId ? operationId[0].toUpperCase() + operationId.substring(1) : '';
64+
const funName = operationId
65+
? operationId[0].toUpperCase() + operationId.substring(1)
66+
: '';
6567
const tag = data?.tags?.[0];
6668

6769
return `${tag ? tag : ''}${funName}`;
@@ -156,7 +158,7 @@ const gen = async () => {
156158
await openAPI.generateService({
157159
schemaPath: `${__dirname}/example-files/openapi-ref-encode-character.json`,
158160
serversPath: './apis/ref-encode-character',
159-
includeTags: ["商品基础管理"],
161+
includeTags: ['商品基础管理'],
160162
});
161163

162164
// 测试支持 apifox x-run-in-apifox
@@ -186,6 +188,12 @@ const gen = async () => {
186188
serversPath: './apis/number-enum',
187189
});
188190

191+
// 测试 number类型 枚举,使用 desc 解析枚举
192+
await openAPI.generateService({
193+
schemaPath: `${__dirname}/example-files/openapi-desc-enum.json`,
194+
serversPath: './apis/desc-enum',
195+
});
196+
189197
// 测试支持 apifox x-apifox-enum
190198
await openAPI.generateService({
191199
schemaPath: `${__dirname}/example-files/openapi-apifox-enum-label.json`,
@@ -218,7 +226,7 @@ const gen = async () => {
218226
// check 文件生成
219227
const fileControllerStr = fs.readFileSync(
220228
path.join(__dirname, 'apis/file/fileController.ts'),
221-
'utf8',
229+
'utf8'
222230
);
223231
assert(fileControllerStr.indexOf('!(item instanceof File)') > 0);
224232
assert(fileControllerStr.indexOf(`'multipart/form-data'`) > 0);

0 commit comments

Comments
 (0)