Skip to content

Commit bb30231

Browse files
feat: support generate api responses type
1 parent 6056bc6 commit bb30231

File tree

46 files changed

+10782
-15
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+10782
-15
lines changed

.changeset/tall-turkeys-rule.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openapi-ts-request': minor
3+
---
4+
5+
feat: support generate api responses type

openapi-ts-request.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export default [
22
{
33
schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
4-
serversPath: './src/apis/app',
4+
serversPath: './apis/app',
55
},
66
];

src/generator/serviceGenarator.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from 'lodash';
1919
import { minimatch } from 'minimatch';
2020
import nunjucks from 'nunjucks';
21+
import type { OpenAPIV3 } from 'openapi-types';
2122
import { join } from 'path';
2223
import { rimrafSync } from 'rimraf';
2324

@@ -651,6 +652,7 @@ export default class ServiceGenerator {
651652
newApi.requestBody as RequestBodyObject
652653
);
653654
const response = this.getResponseTP(newApi.responses);
655+
// console.log(this.resolveObject(newApi.responses))
654656
const file = this.getFileTP(
655657
newApi.requestBody as RequestBodyObject
656658
);
@@ -690,6 +692,24 @@ export default class ServiceGenerator {
690692
response.type = `${this.config.namespace}.${responseName}`;
691693
}
692694

695+
const responsesType = this.getResponsesType(
696+
newApi.responses,
697+
functionName
698+
);
699+
700+
// 如果有多个响应类型,生成对应的类型定义
701+
if (responsesType) {
702+
const responsesTypeName = upperFirst(
703+
`${functionName}Responses`
704+
);
705+
this.interfaceTPConfigs.push({
706+
typeName: responsesTypeName,
707+
type: responsesType,
708+
isEnum: false,
709+
props: [],
710+
});
711+
}
712+
693713
let formattedPath = newApi.path.replace(
694714
/:([^/]*)|{([^}]*)}/gi,
695715
(_, str, str2) => `$\{${str || str2}}`
@@ -1091,6 +1111,137 @@ export default class ServiceGenerator {
10911111
return responseSchema;
10921112
}
10931113

1114+
/**
1115+
* 生成多状态码响应类型定义
1116+
* 将 OpenAPI 的 responses 对象转换为 TypeScript 类型定义
1117+
* 例如:{ 200: ResponseType, 400: unknown, 404: unknown }
1118+
*
1119+
* @param responses OpenAPI 响应对象
1120+
* @param functionName 函数名称,用于生成主响应类型名称
1121+
* @returns 多状态码响应类型定义字符串,如果没有响应则返回 null
1122+
*/
1123+
private getResponsesType(
1124+
responses: ResponsesObject = {},
1125+
functionName: string
1126+
) {
1127+
if (isEmpty(responses)) {
1128+
return null;
1129+
}
1130+
1131+
const { components } = this.openAPIData;
1132+
// 生成主响应类型名称
1133+
const mainResponseTypeName = upperFirst(`${functionName}Response`);
1134+
const responseEntries = this.parseResponseEntries(responses, components);
1135+
1136+
const responseTypes = responseEntries.map(
1137+
({ statusCode, type, description = '' }) => {
1138+
// 检查是否已存在对应的主响应类型,如果存在则复用,避免重复定义
1139+
const existType = this.interfaceTPConfigs.find(
1140+
(item) => item.typeName === mainResponseTypeName
1141+
);
1142+
const lastType = existType ? mainResponseTypeName : type;
1143+
1144+
// 格式化描述文本,让描述支持换行
1145+
const formattedDescription = lineBreakReg.test(description)
1146+
? description.split('\n')?.join('\n * ')
1147+
: description;
1148+
1149+
// 生成带注释的类型定义
1150+
return formattedDescription
1151+
? ` /**\n * ${formattedDescription}\n */\n ${statusCode}: ${lastType};`
1152+
: ` ${statusCode}: ${lastType};`;
1153+
}
1154+
);
1155+
1156+
// 返回完整的对象类型定义
1157+
return `{\n${responseTypes.join('\n')}\n}`;
1158+
}
1159+
1160+
/**
1161+
* 解析响应条目,提取每个状态码对应的类型和描述信息
1162+
*
1163+
* @param responses OpenAPI 响应对象
1164+
* @param components OpenAPI 组件对象,用于解析引用类型
1165+
* @returns 响应条目数组,包含状态码、类型和描述
1166+
*/
1167+
private parseResponseEntries(
1168+
responses: ResponsesObject,
1169+
components: OpenAPIV3.ComponentsObject
1170+
) {
1171+
return keys(responses).map((statusCode) => {
1172+
const response = this.resolveRefObject(
1173+
responses[statusCode] as ResponseObject
1174+
);
1175+
1176+
if (!response) {
1177+
return { statusCode, type: 'unknown', description: '' };
1178+
}
1179+
1180+
const responseType = this.getResponseTypeFromContent(
1181+
response,
1182+
components
1183+
);
1184+
const description = response.description || '';
1185+
1186+
return { statusCode, type: responseType, description };
1187+
});
1188+
}
1189+
1190+
/**
1191+
* 从响应内容中提取 TypeScript 类型
1192+
* 处理不同的媒体类型和 schema 类型
1193+
*
1194+
* @param response 响应对象
1195+
* @param components OpenAPI 组件对象
1196+
* @returns TypeScript 类型字符串
1197+
*/
1198+
private getResponseTypeFromContent(
1199+
response: ResponseObject,
1200+
components: OpenAPIV3.ComponentsObject
1201+
): string {
1202+
if (!response.content) {
1203+
return 'unknown';
1204+
}
1205+
1206+
const resContent: ContentObject = response.content;
1207+
const resContentMediaTypes = keys(resContent);
1208+
const mediaType = resContentMediaTypes.includes('application/json')
1209+
? 'application/json'
1210+
: resContentMediaTypes[0];
1211+
1212+
if (!isObject(resContent) || !mediaType) {
1213+
return 'unknown';
1214+
}
1215+
1216+
let schema = (resContent[mediaType].schema ||
1217+
DEFAULT_SCHEMA) as SchemaObject;
1218+
1219+
if (isReferenceObject(schema)) {
1220+
const refName = getLastRefName(schema.$ref);
1221+
const childrenSchema = components.schemas[refName];
1222+
1223+
// 如果配置了 dataFields,尝试从指定字段提取类型
1224+
if (isNonArraySchemaObject(childrenSchema) && this.config.dataFields) {
1225+
schema = (this.config.dataFields
1226+
.map((field) => childrenSchema.properties[field])
1227+
.filter(Boolean)?.[0] ||
1228+
resContent[mediaType].schema ||
1229+
DEFAULT_SCHEMA) as SchemaObject;
1230+
}
1231+
1232+
return this.getType(schema);
1233+
} else if (isSchemaObject(schema)) {
1234+
// 设置属性的 required 状态
1235+
keys(schema.properties).map((fieldName) => {
1236+
schema.properties[fieldName]['required'] =
1237+
schema.required?.includes(fieldName) ?? false;
1238+
});
1239+
return this.getType(schema);
1240+
} else {
1241+
return this.getType(schema);
1242+
}
1243+
}
1244+
10941245
private getParamsTP(
10951246
parameters: (ParameterObject | ReferenceObject)[] = [],
10961247
path: string = null

test/__snapshots__/both/should both excludeTags and excludePaths works while excludePaths has wildcard.snap

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@ export * from './types';
55
export * from './userZ';
66
/* eslint-disable */
77
// @ts-ignore
8-
export {};
8+
9+
export type UserZz1UsingGetResponses = {
10+
/**
11+
* OK
12+
*/
13+
200: string;
14+
};
15+
16+
export type UserZz1Zz1UsingGetResponses = {
17+
/**
18+
* OK
19+
*/
20+
200: string;
21+
};
922
/* eslint-disable */
1023
// @ts-ignore
1124
import request from '../request';

test/__snapshots__/both/should both excludeTags and excludePaths works with includeTags.snap

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ export * from './types';
55
export * from './userZ';
66
/* eslint-disable */
77
// @ts-ignore
8-
export {};
8+
9+
export type UserZz1UsingGetResponses = {
10+
/**
11+
* OK
12+
*/
13+
200: string;
14+
};
915
/* eslint-disable */
1016
// @ts-ignore
1117
import request from '../request';

test/__snapshots__/both/should exclude items from includePaths and includeTags.snap

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,10 @@ export async function sysAa1UsingGet({
2222
}
2323
/* eslint-disable */
2424
// @ts-ignore
25-
export {};
25+
26+
export type SysAa1UsingGetResponses = {
27+
/**
28+
* OK
29+
*/
30+
200: string;
31+
};

0 commit comments

Comments
 (0)