feat(接口测试): 接口定义导出功能

This commit is contained in:
WangXu10 2024-07-08 18:20:18 +08:00 committed by Craftsman
parent 3c464ed536
commit e17389c271
22 changed files with 1101 additions and 11 deletions

View File

@ -6,15 +6,13 @@ import io.metersphere.api.domain.ApiDefinition;
import io.metersphere.api.dto.ReferenceDTO;
import io.metersphere.api.dto.ReferenceRequest;
import io.metersphere.api.dto.definition.*;
import io.metersphere.api.dto.export.ApiExportResponse;
import io.metersphere.api.dto.request.ApiEditPosRequest;
import io.metersphere.api.dto.request.ApiTransferRequest;
import io.metersphere.api.dto.request.ImportRequest;
import io.metersphere.api.dto.schema.JsonSchemaItem;
import io.metersphere.api.service.ApiFileResourceService;
import io.metersphere.api.service.definition.ApiDefinitionImportService;
import io.metersphere.api.service.definition.ApiDefinitionLogService;
import io.metersphere.api.service.definition.ApiDefinitionNoticeService;
import io.metersphere.api.service.definition.ApiDefinitionService;
import io.metersphere.api.service.definition.*;
import io.metersphere.project.service.FileModuleService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.PermissionConstants;
@ -60,6 +58,8 @@ public class ApiDefinitionController {
private ApiFileResourceService apiFileResourceService;
@Resource
private ApiDefinitionImportService apiDefinitionImportService;
@Resource
private ApiDefinitionExportService apiDefinitionExportService;
@PostMapping(value = "/add")
@Operation(summary = "接口测试-接口管理-添加接口定义")
@ -301,4 +301,10 @@ public class ApiDefinitionController {
StringUtils.isNotBlank(request.getSortString()) ? request.getSortString() : "id desc");
return PageUtils.setPageInfo(page, apiDefinitionService.getReference(request));
}
@PostMapping(value = "/export/{type}")
@RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_EXPORT)
public ApiExportResponse export(@RequestBody ApiDefinitionBatchRequest request, @PathVariable String type) {
return apiDefinitionExportService.export(request, type, SessionUtils.getUserId());
}
}

View File

@ -0,0 +1,81 @@
package io.metersphere.api.dto.definition;
import io.metersphere.api.domain.ApiDefinitionBlob;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* @author wx
*/
@Data
public class ApiDefinitionWithBlob extends ApiDefinitionBlob {
@Schema(description = "接口pk", requiredMode = Schema.RequiredMode.REQUIRED)
private String id;
@Schema(description = "接口名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
@Schema(description = "接口协议", requiredMode = Schema.RequiredMode.REQUIRED)
private String protocol;
@Schema(description = "http协议类型post/get/其它协议则是协议名(mqtt)")
private String method;
@Schema(description = "http协议路径/其它协议则为空")
private String path;
@Schema(description = "接口状态/进行中/已完成", requiredMode = Schema.RequiredMode.REQUIRED)
private String status;
@Schema(description = "自定义id")
private Long num;
@Schema(description = "标签")
private java.util.List<String> tags;
@Schema(description = "自定义排序", requiredMode = Schema.RequiredMode.REQUIRED)
private Long pos;
@Schema(description = "项目fk", requiredMode = Schema.RequiredMode.REQUIRED)
private String projectId;
@Schema(description = "模块fk", requiredMode = Schema.RequiredMode.REQUIRED)
private String moduleId;
@Schema(description = "是否为最新版本 0:否1:是", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean latest;
@Schema(description = "版本fk", requiredMode = Schema.RequiredMode.REQUIRED)
private String versionId;
@Schema(description = "版本引用fk", requiredMode = Schema.RequiredMode.REQUIRED)
private String refId;
@Schema(description = "描述")
private String description;
@Schema(description = "创建时间")
private Long createTime;
@Schema(description = "创建人")
private String createUser;
@Schema(description = "修改时间")
private Long updateTime;
@Schema(description = "修改人")
private String updateUser;
@Schema(description = "删除人")
private String deleteUser;
@Schema(description = "删除时间")
private Long deleteTime;
@Schema(description = "删除状态", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean deleted;
@Schema(description = "模块名称")
private String moduleName;
}

View File

@ -0,0 +1,17 @@
package io.metersphere.api.dto.export;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author wx
*/
@Data
public class ApiExportResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,21 @@
package io.metersphere.api.dto.export;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import java.util.List;
/**
* @author wx
*/
@Data
public class SwaggerApiExportResponse extends ApiExportResponse{
private String openapi;
private SwaggerInfo info;
private JsonNode externalDocs;
private List<String> servers;
private List<SwaggerTag> tags;
private JsonNode paths;
private JsonNode components;
}

View File

@ -0,0 +1,24 @@
package io.metersphere.api.dto.export;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import java.util.List;
/*
该类表示 swagger3 paths 字段下每个请求类型中的 value即表示一个 api 定义
*/
/**
* @author wx
*/
@Data
public class SwaggerApiInfo {
private List<String> tags; // 对应一个 API 在MS项目中所在的 module 名称
private String summary; // 对应 API 的名字
private List<JsonNode> parameters; // 对应 API 的请求参数
private JsonNode requestBody;
private JsonNode responses;
private String description;
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto.export;
import lombok.Data;
/**
* @author wx
*/
@Data
public class SwaggerInfo {
private String version;
private String title;
private String description;
private String termsOfService;
}

View File

@ -0,0 +1,19 @@
package io.metersphere.api.dto.export;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
@Data
public class SwaggerParams {
//对应 API 请求参数名
private String name;
//参数值
private String value;
//参数类型可选值为 path,header,query
private String in;
private String description;
//是否是必填参数
private boolean enable;
private JsonNode schema;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.export;
import lombok.Data;
/**
* @author wx
*/
@Data
public class SwaggerTag {
private String name;
private String description;
}

View File

@ -83,4 +83,6 @@ public interface ExtApiDefinitionMapper {
List<ReferenceDTO> getReference(@Param("request") ReferenceRequest request);
List<ApiDefinition> selectByProjectNum(String projectNum);
List<ApiDefinitionWithBlob> selectApiDefinitionWithBlob(@Param("ids") List<String> ids);
}

View File

@ -9,6 +9,13 @@
<resultMap id="BaseResultMap" type="io.metersphere.api.domain.ApiDefinition">
<result column="tags" jdbcType="VARCHAR" property="tags" typeHandler="io.metersphere.handler.ListTypeHandler" />
</resultMap>
<resultMap id="ApiResultMap" type="io.metersphere.api.dto.definition.ApiDefinitionWithBlob">
<result column="request" jdbcType="LONGVARBINARY" property="request" />
<result column="response" jdbcType="LONGVARBINARY" property="response" />
<result column="tags" jdbcType="VARCHAR" property="tags" typeHandler="io.metersphere.handler.ListTypeHandler" />
</resultMap>
<update id="deleteApiToGc">
update api_definition
set delete_user = #{userId},delete_time = #{time}, deleted = 1 , module_id = 'root'
@ -631,4 +638,21 @@
</where>
group by a.id, ass.ref_type
</select>
<select id="selectApiDefinitionWithBlob" resultMap="ApiResultMap">
SELECT
api_definition.*,
api_definition_blob.request,
api_definition_blob.response,
api_definition_module.name as moduleName
FROM
api_definition
INNER JOIN api_definition_blob ON api_definition.id = api_definition_blob.id
inner join api_definition_module on api_definition.module_id = api_definition_module.id
where api_definition.id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</select>
</mapper>

View File

@ -1,7 +1,10 @@
package io.metersphere.api.parser;
import io.metersphere.api.dto.request.ExportRequest;
import io.metersphere.api.dto.definition.ApiDefinitionWithBlob;
import io.metersphere.project.domain.Project;
import java.util.List;
public interface ExportParser<T> {
T parse(ExportRequest request) throws Exception;
T parse(List<ApiDefinitionWithBlob> list, Project project) throws Exception;
}

View File

@ -0,0 +1,575 @@
package io.metersphere.api.parser.api;
import com.fasterxml.jackson.databind.JsonNode;
import io.metersphere.api.dto.definition.ApiDefinitionWithBlob;
import io.metersphere.api.dto.export.*;
import io.metersphere.api.dto.request.http.body.Body;
import io.metersphere.api.parser.ExportParser;
import io.metersphere.api.utils.JSONUtil;
import io.metersphere.api.utils.XMLUtil;
import io.metersphere.project.constants.PropertyConstant;
import io.metersphere.project.domain.Project;
import io.metersphere.sdk.util.JSON;
import io.swagger.v3.oas.models.responses.ApiResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class Swagger3ExportParser implements ExportParser<ApiExportResponse> {
@Override
public ApiExportResponse parse(List<ApiDefinitionWithBlob> list, Project project) throws Exception {
SwaggerApiExportResponse response = new SwaggerApiExportResponse();
//openapi
response.setOpenapi("3.0.2");
//info
SwaggerInfo swaggerInfo = new SwaggerInfo();
swaggerInfo.setVersion("3.0");
swaggerInfo.setTitle("ms-" + project.getName());
swaggerInfo.setDescription(StringUtils.EMPTY);
swaggerInfo.setTermsOfService(StringUtils.EMPTY);
response.setInfo(swaggerInfo);
//servers
response.setServers(new ArrayList<>());
//tags
response.setTags(new ArrayList<>());
response.setComponents(JSONUtil.createObj());
response.setExternalDocs(JSONUtil.createObj());
//path
JSONObject paths = new JSONObject();
JSONObject components = new JSONObject();
List<JSONObject> schemas = new LinkedList<>();
for (ApiDefinitionWithBlob apiDefinition : list) {
SwaggerApiInfo swaggerApiInfo = new SwaggerApiInfo();
swaggerApiInfo.setSummary(apiDefinition.getName());
swaggerApiInfo.setTags(Arrays.asList(apiDefinition.getModuleName()));
//请求体
JSONObject requestObject = JSONUtil.parseObject(new String(apiDefinition.getRequest() == null ? new byte[0] : apiDefinition.getRequest(), StandardCharsets.UTF_8));
JSONObject requestBody = buildRequestBody(requestObject, schemas);
swaggerApiInfo.setRequestBody(JSONUtil.parseObjectNode(requestBody.toString()));
JSONArray responseObject = new JSONArray();
try {
// 设置响应体
responseObject = JSONUtil.parseArray(new String(apiDefinition.getResponse() == null ? new byte[0] : apiDefinition.getResponse(), StandardCharsets.UTF_8));
} catch (Exception e) {
responseObject = new JSONArray(new ApiResponse());
}
JSONObject jsonObject = buildResponseBody(responseObject, schemas);
swaggerApiInfo.setResponses(JSONUtil.parseObjectNode(jsonObject.toString()));
// 设置请求参数列表
List<JSONObject> paramsList = buildParameters(requestObject);
List<JsonNode> nodes = new LinkedList<>();
paramsList.forEach(item -> {
nodes.add(JSONUtil.parseObjectNode(item.toString()));
});
swaggerApiInfo.setParameters(nodes);
swaggerApiInfo.setDescription(apiDefinition.getDescription());
JSONObject methodDetail = JSONUtil.parseObject(JSON.toJSONString(swaggerApiInfo));
if (paths.optJSONObject(apiDefinition.getPath()) == null) {
paths.put(apiDefinition.getPath(), new JSONObject());
} // 一个路径下有多个发方法如postget因此是一个 JSONObject 类型
paths.optJSONObject(apiDefinition.getPath()).put(apiDefinition.getMethod().toLowerCase(), methodDetail);
}
response.setPaths(JSONUtil.parseObjectNode(paths.toString()));
if (CollectionUtils.isNotEmpty(schemas)) {
components.put("schemas", schemas.get(0));
}
response.setComponents(JSONUtil.parseObjectNode(components.toString()));
return response;
}
private List<JSONObject> buildParameters(JSONObject request) {
List<JSONObject> paramsList = new ArrayList<>();
Hashtable<String, String> typeMap = new Hashtable<String, String>() {{
put("headers", "header");
put("rest", "path");
put("arguments", "query");
}};
Set<String> typeKeys = typeMap.keySet();
for (String type : typeKeys) {
JSONArray params = request.optJSONArray(type); // 获得请求参数列表
if (params != null) {
for (int i = 0; i < params.length(); ++i) {
JSONObject param = params.optJSONObject(i); // 对于每个参数:
if (StringUtils.isEmpty(param.optString("key"))) {
continue;
} // 否则无参数的情况可能多出一行空行
SwaggerParams swaggerParam = new SwaggerParams();
swaggerParam.setIn(typeMap.get(type)); // 利用 map根据 request key 设置对应的参数类型
swaggerParam.setDescription(param.optString("description"));
swaggerParam.setName(param.optString("key"));
swaggerParam.setEnable(param.optBoolean(PropertyConstant.ENABLE));
swaggerParam.setValue(param.optString("value"));
JSONObject schema = new JSONObject();
schema.put(PropertyConstant.TYPE, PropertyConstant.STRING);
swaggerParam.setSchema(JSONUtil.parseObjectNode(schema.toString()));
paramsList.add(JSONUtil.parseObject(JSON.toJSONString(swaggerParam)));
}
}
}
return paramsList;
}
private JSONObject buildResponseBody(JSONArray response, List<JSONObject> schemas) {
if (response.length() == 0) {
return new JSONObject();
}
JSONObject responseBody = new JSONObject();
for (int i = 0; i < response.length(); i++) {
JSONObject responseJSONObject = response.getJSONObject(i);
JSONObject headers = new JSONObject();
JSONArray headValueList = responseJSONObject.optJSONArray("headers");
if (headValueList != null) {
for (Object item : headValueList) {
if (item instanceof JSONObject && ((JSONObject) item).optString("key") != null) {
JSONObject head = new JSONObject(), headSchema = new JSONObject();
head.put("description", ((JSONObject) item).optString("description"));
head.put("example", ((JSONObject) item).optString("value"));
headSchema.put(PropertyConstant.TYPE, PropertyConstant.STRING);
head.put("schema", headSchema);
headers.put(((JSONObject) item).optString("key"), head);
}
}
}
String statusCode = responseJSONObject.optString("statusCode");
if (StringUtils.isNotBlank(statusCode)) {
JSONObject statusCodeInfo = new JSONObject();
statusCodeInfo.put("headers", headers);
statusCodeInfo.put("content", buildContent(responseJSONObject, schemas));
statusCodeInfo.put("description", StringUtils.EMPTY);
if (StringUtils.isNotBlank(responseJSONObject.optString("value"))) {
statusCodeInfo.put("description", responseJSONObject.optString("value"));
}
if (StringUtils.isNotBlank(responseJSONObject.optString("name"))) {
responseBody.put(responseJSONObject.optString("name"), statusCodeInfo);
}
}
}
return responseBody;
}
private JSONObject buildRequestBody(JSONObject request, List<JSONObject> schemas) {
JSONObject requestBody = new JSONObject();
requestBody.put("content", buildContent(request, schemas));
return requestBody;
}
private JSONObject buildContent(JSONObject respOrReq, List<JSONObject> schemas) {
Hashtable<String, String> typeMap = new Hashtable<String, String>() {{
put(Body.BodyType.XML.name(), org.springframework.http.MediaType.APPLICATION_XML_VALUE);
put(Body.BodyType.JSON.name(), org.springframework.http.MediaType.APPLICATION_JSON_VALUE);
put(Body.BodyType.RAW.name(), "application/urlencoded");
put(Body.BodyType.BINARY.name(), org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE);
put(Body.BodyType.FORM_DATA.name(), org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE);
put(Body.BodyType.WWW_FORM.name(), org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE);
}};
Object bodyInfo = null;
Object jsonInfo = null;
JSONObject body = respOrReq.optJSONObject("body");
if (body != null) { // 将请求体转换成相应的格式导出
String bodyType = body.optString(PropertyConstant.BODYTYPE);
if (StringUtils.isNotBlank(bodyType) && bodyType.equalsIgnoreCase(Body.BodyType.JSON.name())) {
try {
// json
String jsonValue = body.optJSONObject("jsonBody").optString("jsonValue");
if (StringUtils.isNotBlank(jsonValue)) {
jsonInfo = buildJson(jsonValue);
}
// jsonSchema
String jsonSchema = body.optJSONObject("jsonBody").optString("jsonSchema");
if (StringUtils.isNotBlank(jsonSchema)) {
JSONObject jsonSchemaObject = JSONUtil.parseObject(jsonSchema);
bodyInfo = buildJsonSchema(jsonSchemaObject);
}
} catch (Exception e1) { // 若请求体 json 不合法则忽略错误原样字符串导出/导入
bodyInfo = new JSONObject();
((JSONObject) bodyInfo).put(PropertyConstant.TYPE, PropertyConstant.STRING);
if (body != null && body.optString("rawBody") != null) {
((JSONObject) bodyInfo).put("example", body.optString("rawBody"));
}
}
} else if (bodyType != null && bodyType.equalsIgnoreCase(Body.BodyType.RAW.name())) {
bodyInfo = new JSONObject();
((JSONObject) bodyInfo).put(PropertyConstant.TYPE, PropertyConstant.STRING);
if (body != null && body.optString("rawBody") != null) {
((JSONObject) bodyInfo).put("example", body.optString("rawBody"));
}
} else if (bodyType != null && bodyType.equalsIgnoreCase(Body.BodyType.XML.name())) {
String xmlText = body.optString("xmlBody");
JSONObject xmlObject = JSONUtil.parseObject(xmlText);
xmlText = xmlObject.optString("value");
String xml = XMLUtil.delXmlHeader(xmlText);
int startIndex = xml.indexOf("<", 0);
int endIndex = xml.indexOf(">", 0);
if (endIndex > startIndex + 1) {
String substring = xml.substring(startIndex + 1, endIndex);
bodyInfo = buildRefSchema(substring);
}
JSONObject xmlToJson = XMLUtil.xmlConvertJson(xmlText);
JSONObject jsonObject = buildRequestBodyXmlSchema(xmlToJson);
if (schemas == null) {
schemas = new LinkedList<>();
}
schemas.add(jsonObject);
} else if (bodyType != null && (bodyType.equalsIgnoreCase(Body.BodyType.WWW_FORM.name()) || bodyType.equalsIgnoreCase(Body.BodyType.FORM_DATA.name()))) { // key-value 类格式
String wwwFormBody = body.optString("wwwFormBody");
JSONObject wwwFormObject = JSONUtil.parseObject(wwwFormBody);
JSONObject formData = getformDataProperties(wwwFormObject.optJSONArray("formValues"));
bodyInfo = buildFormDataSchema(formData);
} else if (bodyType != null && bodyType.equalsIgnoreCase(Body.BodyType.BINARY.name())) {
bodyInfo = buildBinary();
}
}
String type = null;
if (respOrReq.optJSONObject("body") != null) {
type = respOrReq.optJSONObject("body").optString(PropertyConstant.BODYTYPE);
}
JSONObject content = new JSONObject();
Object schema = bodyInfo; // 请求体部分
JSONObject typeName = new JSONObject();
if (schema != null) {
typeName.put("schema", schema);
}
if (jsonInfo != null) {
typeName.put("example", jsonInfo);
}
if (StringUtils.isNotBlank(type) && typeMap.containsKey(type)) {
content.put(typeMap.get(type), typeName);
}
return content;
}
private JSONObject buildBinary() {
JSONObject parsedParam = new JSONObject();
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING);
parsedParam.put("format", "binary");
return parsedParam;
}
/**
* requestBody 中jsonSchema
*
* @param jsonSchemaObject
* @return
*/
private JSONObject buildJsonSchema(JSONObject jsonSchemaObject) {
JSONObject parsedParam = new JSONObject();
String type = jsonSchemaObject.optString(PropertyConstant.TYPE);
if (StringUtils.isNotBlank(type)) {
if (StringUtils.equals(type, PropertyConstant.OBJECT)) {
parsedParam = jsonSchemaObject;
} else if (StringUtils.equals(type, PropertyConstant.ARRAY)) {
JSONArray items = jsonSchemaObject.optJSONArray(PropertyConstant.ITEMS);
JSONObject itemProperties = new JSONObject();
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.ARRAY);
if (items != null) {
JSONObject itemsObject = new JSONObject();
if (items.length() > 0) {
items.forEach(item -> {
if (item instanceof JSONObject) {
JSONObject itemJson = buildJsonSchema((JSONObject) item);
if (itemJson != null) {
Set<String> keys = itemJson.keySet();
for (String key : keys) {
itemProperties.put(key, itemJson.get(key));
}
}
}
});
}
itemsObject.put(PropertyConstant.PROPERTIES, itemProperties);
parsedParam.put(PropertyConstant.ITEMS, itemsObject.optJSONObject(PropertyConstant.PROPERTIES));
} else {
parsedParam.put(PropertyConstant.ITEMS, new JSONObject());
}
} else if (StringUtils.equals(type, PropertyConstant.INTEGER)) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.INTEGER);
parsedParam.put("format", "int64");
setCommonJsonSchemaParam(parsedParam, jsonSchemaObject);
} else if (StringUtils.equals(type, PropertyConstant.BOOLEAN)) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.BOOLEAN);
setCommonJsonSchemaParam(parsedParam, jsonSchemaObject);
} else if (StringUtils.equals(type, PropertyConstant.NUMBER)) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.NUMBER);
setCommonJsonSchemaParam(parsedParam, jsonSchemaObject);
} else {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING);
setCommonJsonSchemaParam(parsedParam, jsonSchemaObject);
}
}
return parsedParam;
}
/**
* requestBody 中json
*
* @param jsonValue
* @return
*/
private JSONObject buildJson(String jsonValue) {
JSONObject jsonObject = JSONUtil.parseObject(jsonValue);
return jsonObject;
}
private JSONObject getformDataProperties(JSONArray requestBody) {
JSONObject result = new JSONObject();
for (Object item : requestBody) {
if (item instanceof JSONObject) {
String name = ((JSONObject) item).optString("key");
if (name != null) {
result.put(name, item);
}
}
}
return result;
}
private static JSONObject buildRequestBodyXmlSchema(JSONObject requestBody) {
if (requestBody == null) return null;
JSONObject schema = new JSONObject();
for (String key : requestBody.keySet()) {
Object param = requestBody.get(key);
JSONObject parsedParam = new JSONObject();
if (param instanceof String) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING);
parsedParam.put("example", param == null ? StringUtils.EMPTY : param);
} else if (param instanceof Integer) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.INTEGER);
parsedParam.put("format", "int64");
parsedParam.put("example", param);
} else if (param instanceof JSONObject) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.OBJECT);
Object attribute = ((JSONObject) param).opt("attribute");
//build properties
JSONObject paramObject = buildRequestBodyXmlSchema((JSONObject) param);
if (attribute != null && attribute instanceof JSONArray) {
JSONObject jsonObject = buildXmlProperties(((JSONArray) attribute).getJSONObject(0));
paramObject.remove("attribute");
for (String paramKey : paramObject.keySet()) {
Object paramChild = paramObject.get(paramKey);
if (paramChild instanceof String) {
JSONObject one = new JSONObject();
one.put(PropertyConstant.TYPE, PropertyConstant.OBJECT);
one.put("properties", jsonObject);
paramObject.remove("example");
paramObject.remove(paramKey);
paramObject.put(paramKey, one);
}
if (paramChild instanceof JSONObject) {
Object properties = ((JSONObject) paramChild).opt("properties");
if (properties != null) {
for (String aa : jsonObject.keySet()) {
Object value = jsonObject.get(aa);
if (((JSONObject) properties).opt(aa) == null) {
((JSONObject) properties).put(aa, value);
}
}
} else {
((JSONObject) paramChild).put("properties", jsonObject);
}
if (((JSONObject) paramChild).opt("type") == "string") {
((JSONObject) paramChild).put("type", "object");
((JSONObject) paramChild).remove("example");
}
}
}
}
parsedParam.put("properties", paramObject);
if (StringUtils.isNotBlank(requestBody.optString("description"))) {
parsedParam.put("description", requestBody.optString("description"));
}
} else if (param instanceof Boolean) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.BOOLEAN);
parsedParam.put("example", param);
} else if (param instanceof java.math.BigDecimal) { // double 类型会被 fastJson 转换为 BigDecimal
parsedParam.put(PropertyConstant.TYPE, "double");
parsedParam.put("example", param);
} else { // JSONOArray
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.OBJECT);
if (param == null) {
param = new JSONArray();
}
JSONObject jsonObjects = new JSONObject();
if (((JSONArray) param).length() > 0) {
((JSONArray) param).forEach(t -> {
JSONObject item = buildRequestBodyXmlSchema((JSONObject) t);
for (String s : item.keySet()) {
jsonObjects.put(s, item.get(s));
}
});
}
parsedParam.put(PropertyConstant.PROPERTIES, jsonObjects);
}
schema.put(key, parsedParam);
}
return schema;
}
private static JSONObject buildXmlProperties(JSONObject kvs) {
JSONObject properties = new JSONObject();
for (String key : kvs.keySet()) {
JSONObject property = new JSONObject();
Object param = kvs.opt(key);
if (param instanceof String) {
property.put(PropertyConstant.TYPE, PropertyConstant.STRING);
property.put("example", param == null ? StringUtils.EMPTY : param);
}
if (param instanceof JSONObject) {
JSONObject obj = ((JSONObject) param);
property.put(PropertyConstant.TYPE, StringUtils.isNotEmpty(obj.optString(PropertyConstant.TYPE)) ? obj.optString(PropertyConstant.TYPE) : PropertyConstant.STRING);
String value = obj.optString("value");
if (StringUtils.isBlank(value)) {
JSONObject mock = obj.optJSONObject(PropertyConstant.MOCK);
if (mock != null) {
Object mockValue = mock.get(PropertyConstant.MOCK);
property.put("example", mockValue);
} else {
property.put("example", value);
}
} else {
property.put("example", value);
}
}
JSONObject xml = new JSONObject();
xml.put("attribute", true);
property.put("xml", xml);
properties.put(key, property);
}
return properties;
}
private Object buildRefSchema(String substring) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("$ref", "#/components/schemas/" + substring);
return jsonObject;
}
private JSONObject buildRequestBodyJsonInfo(JSONObject requestBody) {
if (requestBody == null) return null;
JSONObject schema = new JSONObject();
schema.put(PropertyConstant.TYPE, PropertyConstant.OBJECT);
JSONObject properties = buildSchema(requestBody);
schema.put(PropertyConstant.PROPERTIES, properties);
return schema;
}
private JSONObject buildSchema(JSONObject requestBody) {
JSONObject schema = new JSONObject();
for (String key : requestBody.keySet()) {
Object param = requestBody.get(key);
JSONObject parsedParam = new JSONObject();
if (param instanceof String) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING);
parsedParam.put("example", param == null ? StringUtils.EMPTY : param);
} else if (param instanceof Integer) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.INTEGER);
parsedParam.put("format", "int64");
parsedParam.put("example", param);
} else if (param instanceof JSONObject) {
parsedParam = buildRequestBodyJsonInfo((JSONObject) param);
} else if (param instanceof Boolean) {
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.BOOLEAN);
parsedParam.put("example", param);
} else if (param instanceof java.math.BigDecimal) { // double 类型会被 fastJson 转换为 BigDecimal
parsedParam.put(PropertyConstant.TYPE, "double");
parsedParam.put("example", param);
} else { // JSONOArray
parsedParam.put(PropertyConstant.TYPE, PropertyConstant.ARRAY);
JSONObject item = new JSONObject();
if (param == null) {
param = new JSONArray();
}
if (((JSONArray) param).length() > 0) {
if (((JSONArray) param).get(0) instanceof JSONObject) { ///
item = buildRequestBodyJsonInfo((JSONObject) ((JSONArray) param).get(0));
}
}
parsedParam.put(PropertyConstant.ITEMS, item);
}
schema.put(key, parsedParam);
}
return schema;
}
private JSONObject buildFormDataSchema(JSONObject kvs) {
JSONObject schema = new JSONObject();
JSONObject properties = new JSONObject();
for (String key : kvs.keySet()) {
JSONObject property = new JSONObject();
JSONObject obj = ((JSONObject) kvs.get(key));
property.put(PropertyConstant.TYPE, StringUtils.isNotEmpty(obj.optString(PropertyConstant.PARAMTYPE)) ? obj.optString(PropertyConstant.PARAMTYPE) : PropertyConstant.STRING);
String value = obj.optString("value");
if (StringUtils.isBlank(value)) {
JSONObject mock = obj.optJSONObject(PropertyConstant.MOCK);
if (mock != null && StringUtils.isNotBlank(mock.optString("mock"))) {
Object mockValue = mock.get(PropertyConstant.MOCK);
property.put("example", mockValue);
} else {
property.put("example", value);
}
} else {
property.put("example", value);
}
property.put("description", obj.optString("description"));
property.put(PropertyConstant.REQUIRED, obj.optString(PropertyConstant.REQUIRED));
if (obj.optJSONObject(PropertyConstant.PROPERTIES) != null) {
JSONObject childProperties = buildFormDataSchema(obj.optJSONObject(PropertyConstant.PROPERTIES));
property.put(PropertyConstant.PROPERTIES, childProperties.optJSONObject(PropertyConstant.PROPERTIES));
} else {
JSONObject childProperties = buildJsonSchema(obj);
if (StringUtils.equalsIgnoreCase(obj.optString(PropertyConstant.PARAMTYPE), PropertyConstant.ARRAY)) {
if (childProperties.optJSONObject(PropertyConstant.ITEMS) != null) {
property.put(PropertyConstant.ITEMS, childProperties.optJSONObject(PropertyConstant.ITEMS));
}
} else {
if (childProperties.optJSONObject(PropertyConstant.PROPERTIES) != null) {
property.put(PropertyConstant.PROPERTIES, childProperties.optJSONObject(PropertyConstant.PROPERTIES));
}
}
}
properties.put(key, property);
}
schema.put(PropertyConstant.PROPERTIES, properties);
return schema;
}
public void setCommonJsonSchemaParam(JSONObject parsedParam, JSONObject requestBody) {
if (StringUtils.isNotBlank(requestBody.optString("description"))) {
parsedParam.put("description", requestBody.optString("description"));
}
Object jsonSchemaValue = getJsonSchemaValue(requestBody);
if (jsonSchemaValue != null) {
parsedParam.put("example", jsonSchemaValue);
}
}
public Object getJsonSchemaValue(JSONObject item) {
JSONObject mock = item.optJSONObject(PropertyConstant.MOCK);
if (mock != null) {
if (StringUtils.isNotBlank(mock.optString("mock"))) {
Object value = mock.get(PropertyConstant.MOCK);
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,51 @@
package io.metersphere.api.service.definition;
import io.metersphere.api.dto.definition.ApiDefinitionBatchRequest;
import io.metersphere.api.dto.definition.ApiDefinitionWithBlob;
import io.metersphere.api.dto.export.ApiExportResponse;
import io.metersphere.api.mapper.ExtApiDefinitionMapper;
import io.metersphere.api.parser.api.Swagger3ExportParser;
import io.metersphere.project.domain.Project;
import io.metersphere.project.mapper.ProjectMapper;
import io.metersphere.sdk.exception.MSException;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author wx
*/
@Service
public class ApiDefinitionExportService {
@Resource
private ApiDefinitionService apiDefinitionService;
@Resource
private ExtApiDefinitionMapper extApiDefinitionMapper;
@Resource
private ProjectMapper projectMapper;
public ApiExportResponse export(ApiDefinitionBatchRequest request, String type, String userId) {
List<String> ids = apiDefinitionService.getBatchApiIds(request, request.getProjectId(), request.getProtocols(), false, userId);
List<ApiDefinitionWithBlob> list = extApiDefinitionMapper.selectApiDefinitionWithBlob(ids);
switch (type) {
case "swagger":
return exportSwagger(request, list);
default:
return new ApiExportResponse();
}
}
private ApiExportResponse exportSwagger(ApiDefinitionBatchRequest request, List<ApiDefinitionWithBlob> list) {
Project project = projectMapper.selectByPrimaryKey(request.getProjectId());
Swagger3ExportParser swagger3Parser = new Swagger3ExportParser();
try {
return swagger3Parser.parse(list, project);
} catch (Exception e) {
throw new MSException(e);
}
}
}

View File

@ -0,0 +1,80 @@
package io.metersphere.api.utils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class JSONUtil {
private static final ObjectMapper objectMapper = JsonMapper.builder()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
.build();
private static final TypeFactory typeFactory = objectMapper.getTypeFactory();
public static final int DEFAULT_MAX_STRING_LEN = Integer.MAX_VALUE;
static {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 自动检测所有类的全部属性
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
// 如果一个对象中没有任何的属性那么在序列化的时候就会报错
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
// 使用BigDecimal来序列化
objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
// 设置JSON处理字符长度限制
objectMapper.getFactory()
.setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(DEFAULT_MAX_STRING_LEN).build());
// 处理时间格式
objectMapper.registerModule(new JavaTimeModule());
}
public static JSONObject parseObject(String value) {
try {
if (StringUtils.isEmpty(value)) {
throw new MSException("value is null");
}
Map<String, Object> map = JSON.parseObject(value, Map.class);
return new JSONObject(map);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static JSONArray parseArray(String text) {
List<Object> list = JSON.parseObject(text, List.class);
return new JSONArray(list);
}
public static ObjectNode parseObjectNode(String text) {
try {
return (ObjectNode) objectMapper.readTree(text);
} catch (Exception e) {
LogUtils.error(e);
}
return objectMapper.createObjectNode();
}
public static ObjectNode createObj() {
return objectMapper.createObjectNode();
}
}

View File

@ -0,0 +1,97 @@
package io.metersphere.api.utils;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.XMLUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author wx
*/
public class XMLUtil {
@Nullable
public static String delXmlHeader(String xml) {
int begin = xml.indexOf("?>");
if (begin != -1) {
if (begin + 2 >= xml.length()) {
return null;
}
xml = xml.substring(begin + 2);
} // <?xml version="1.0" encoding="utf-8"?> 若存在则去除
String rgex = ">";
Pattern pattern = Pattern.compile(rgex);
Matcher m = pattern.matcher(xml);
xml = m.replaceAll("> ");
rgex = "\\s*</";
pattern = Pattern.compile(rgex);
m = pattern.matcher(xml);
xml = m.replaceAll(" </");
return xml;
}
// 传入完整的 xml 文本转换成 json 对象
public static JSONObject xmlConvertJson(String xml) {
if (StringUtils.isBlank(xml)) return null;
xml = delXmlHeader(xml);
if (xml == null) return null;
if (stringToDocument(xml) == null) {
LogUtils.error("xml内容转换失败");
return null;
}
Element node = stringToDocument(xml).getRootElement();
JSONObject result = getJsonObjectByDC(node);
return result;
}
public static Document stringToDocument(String xml) {
try {
return XMLUtils.getDocument(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8.name())));
} catch (Exception e) {
LogUtils.error(e);
return null;
}
}
private static JSONObject getJsonObjectByDC(Element node) {
JSONObject result = new JSONObject();
List<Element> listElement = node.elements();// 所有一级子节点的list
if (!listElement.isEmpty()) {
List<JSONObject> list = new LinkedList<>();
for (Element e : listElement) {// 遍历所有一级子节点
JSONObject jsonObject = getJsonObjectByDC(e);
//加xml标签上的属性 eg: <field length="2" scale="0" type="string">RB</field>
//这里添加 length scale type
if (!e.attributes().isEmpty()) {
JSONObject attributeJson = new JSONObject();
for (Attribute attribute : e.attributes()) {
attributeJson.put(attribute.getName(), attribute.getValue());
}
jsonObject.append("attribute", attributeJson);
}
list.add(jsonObject);
}
if (list.size() == 1) {
result.put(node.getName(), list.get(0));
} else {
result.put(node.getName(), list);
}
} else {
if (!StringUtils.isAllBlank(node.getName(), node.getText())) {
result.put(node.getName(), node.getText());
}
}
return result;
}
}

View File

@ -117,6 +117,8 @@ public class ApiDefinitionControllerTests extends BaseTest {
private static final String ALL_API = "api_definition_module.api.all";
private static final String UNPLANNED_API = "api_unplanned_request";
private static final String EXPORT = "/export/";
private static ApiDefinition apiDefinition;
@Resource
@ -1854,4 +1856,21 @@ public class ApiDefinitionControllerTests extends BaseTest {
Assertions.assertNotNull(pageData);
}
@Test
@Order(104)
public void testExport() throws Exception {
ApiDefinitionBatchRequest request = new ApiDefinitionBatchRequest();
request.setProjectId(DEFAULT_PROJECT_ID);
request.setProtocols(List.of("HTTP"));
request.setSelectAll(false);
request.setSelectIds(List.of("1001"));
MvcResult mvcResult = this.requestPostWithOkAndReturn(EXPORT + "swagger", request);
String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class);
// 返回请求正常
Assertions.assertNotNull(resultHolder);
Pager<?> pageData = JSON.parseObject(JSON.toJSONString(resultHolder.getData()), Pager.class);
Assertions.assertNotNull(pageData);
}
}

View File

@ -19,5 +19,8 @@ public class PropertyConstant {
public final static String ITEMS = "items";
public final static String PROPERTIES = "properties";
public final static String ENABLE = "enable";
public final static String MOCK = "mock";
public final static String BODYTYPE = "bodyType";
public final static String PARAMTYPE = "paramtype";
}

View File

@ -36,6 +36,7 @@ import {
DeleteRecycleApiUrl,
DeleteRecycleCaseUrl,
ExecuteCaseUrl,
ExportDefinitionUrl,
GetCaseDetailUrl,
GetCaseReportByIdUrl,
GetCaseReportDetailUrl,
@ -101,6 +102,7 @@ import {
ApiCasePageParams,
ApiDefinitionBatchDeleteParams,
ApiDefinitionBatchMoveParams,
ApiDefinitionBatchParams,
ApiDefinitionBatchUpdateParams,
ApiDefinitionCreateParams,
ApiDefinitionDeleteParams,
@ -556,3 +558,8 @@ export function getReportById(id: string) {
export function getCaseReportDetail(reportId: string, stepId: string) {
return MSR.get<ApiCaseReportDetail[]>({ url: `${GetCaseReportDetailUrl + reportId}/${stepId}` });
}
// 导出定义
export function exportApiDefinition(data: ApiDefinitionBatchParams, type: string) {
return MSR.post({ url: `${ExportDefinitionUrl}/${type}`, data });
}

View File

@ -18,6 +18,7 @@ export const TransferFileModuleOptionUrl = '/api/definition/transfer/options'; /
export const UploadTempFileUrl = '/api/definition/upload/temp/file'; // 临时文件上传
export const DeleteDefinitionUrl = '/api/definition/delete-to-gc'; // 删除接口定义
export const ImportDefinitionUrl = '/api/definition/import'; // 导入接口定义
export const ExportDefinitionUrl = '/api/definition/export'; // 导入接口定义
export const SortDefinitionUrl = '/api/definition/edit/pos'; // 接口定义拖拽
export const CopyDefinitionUrl = '/api/definition/copy'; // 复制接口定义
export const BatchUpdateDefinitionUrl = '/api/definition/batch-update'; // 批量更新接口定义

View File

@ -276,6 +276,7 @@
batchMoveDefinition,
batchUpdateDefinition,
deleteDefinition,
exportApiDefinition,
getDefinitionPage,
sortDefinition,
updateDefinition,
@ -284,7 +285,7 @@
import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app';
import { characterLimit, operationWidth } from '@/utils';
import { characterLimit, downloadByteFile, operationWidth } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { ProtocolItem } from '@/models/apiTest/common';
@ -555,10 +556,16 @@
);
const batchActions = {
baseAction: [
// {
// label: 'common.export',
// eventTag: 'export',
// },
{
label: 'common.export',
eventTag: 'export',
children: [
{
label: 'apiTestManagement.swagger.export',
eventTag: 'exportSwagger',
},
],
},
{
label: 'common.edit',
eventTag: 'edit',
@ -906,6 +913,28 @@
selectedModuleKeys.value = keys;
}
/**
* 导出接口
*/
async function exportApi(type: string, record?: ApiDefinitionDetail, params?: BatchActionQueryParams) {
const result = await exportApiDefinition(
{
selectIds: tableSelected.value as string[],
selectAll: !!params?.selectAll,
excludeIds: params?.excludeIds || [],
condition: {
keyword: keyword.value,
filter: propsRes.value.filter,
},
projectId: appStore.currentProjectId,
moduleIds: await getModuleIds(),
protocols: props.selectedProtocols,
},
type
);
downloadByteFile(new Blob([JSON.stringify(result)]), 'Swagger_Api_Case.json');
}
/**
* 处理表格选中后批量操作
* @param event 批量操作事件对象
@ -914,6 +943,9 @@
tableSelected.value = params?.selectedIds || [];
batchParams.value = params;
switch (event.eventTag) {
case 'exportSwagger':
exportApi('swagger', undefined, params);
break;
case 'delete':
deleteApi(undefined, true, params);
break;

View File

@ -80,6 +80,7 @@ export default {
'apiTestManagement.moreSetting': 'More Settings',
'apiTestManagement.importType': 'Import Type',
'apiTestManagement.urlImport': 'URL Import',
'apiTestManagement.swagger.export': 'Export Swagger3.0',
'apiTestManagement.syncImportCase': 'Sync Import API Cases',
'apiTestManagement.syncUpdateDirectory': 'Sync Update API Directory',
'apiTestManagement.importSwaggerFileTip1': 'Supports Swagger 3.0 version JSON files,',

View File

@ -75,6 +75,7 @@ export default {
'apiTestManagement.moreSetting': '更多设置',
'apiTestManagement.importType': '导入方式',
'apiTestManagement.urlImport': 'URL 导入',
'apiTestManagement.swagger.export': '导出 Swagger3.0 格式',
'apiTestManagement.syncImportCase': '同步导入接口用例',
'apiTestManagement.syncUpdateDirectory': '同步更新接口所在目录',
'apiTestManagement.importSwaggerFileTip1': '支持 Swagger 3.0 版本的 json 文件,',