diff --git a/define/parser_api.go b/define/parser_api.go new file mode 100644 index 0000000..ba18cce --- /dev/null +++ b/define/parser_api.go @@ -0,0 +1,56 @@ +// Package parser ... +// +// Description : parser ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-04-11 20:21 +package define + +// ApiConfig 解析出的接口配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 20:21 2025/4/11 +type ApiConfig struct { + Tag string `json:"tag" dc:"接口所属标签"` // 接口所属标签 + Name string `json:"name" binding:"required,min=1" dc:"接口名称" err:"项目接口名称必须存在,且不能为空"` // 接口名称 + Uri string `json:"uri" binding:"required,min=1,max=512" dc:"项目接口uri" err:"项目接口路径必须存在,且长度在[1,512]之间"` // 项目接口路由 + MethodID int64 `json:"method_id" dc:"请求方法ID"` // 请求方法 + Method string `json:"method" binding:"required_without=MethodID" dc:"请求方法" err:"请求方法与请求方法ID至少存在一个"` + ContentType string `json:"content_type" dc:"请求类型" binding:"required_without=ContentTypeID" err:"请求类型ID与请求类型至少存在一个"` + Description string `json:"description" dc:"项目接口描述"` // 接口描述 + ParamList []*ApiParamItem `json:"param_list" dc:"项目接口参数列表"` // 参数列表 + ResultList []*ApiResultItem `json:"result_list" dc:"项目接口返回值列表"` // 返回值列表 +} + +// ApiParamItem 接口参数配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 20:23 2025/4/11 +type ApiParamItem struct { + Title string `json:"title" binding:"required,min=1" dc:"参数标题" err:"参数标题必须存在,且不能为空"` // 参数标题 + Name string `json:"name" binding:"required,min=1" dc:"参数名称" err:"参数名称必须存在,且不能为空"` // 参数名称 + Location string `json:"location" binding:"required,oneof=HEADER COOKIE BODY QUERY PATH" dc:"参数位置" err:"参数位置必须存在,且需要是一个有效合法值"` // 参数位置 + ParamType string `json:"param_type" binding:"required,validate_data_type" dc:"参数类型" err:"参数类型必须存在,且需要是一个有效的合法值"` // 参数类型 + IsRequired bool `json:"is_required" binding:"required" dc:"参数是否必传" err:"参数是否必传必须存在,且需要是一个有效合法值"` // 是否必传 + DefaultValue string `json:"default_value" dc:"默认值"` // 默认值 + ExampleValue string `json:"example_value" dc:"示例值"` // 示例值 + Description string `json:"description" dc:"参数描述"` // 参数描述 +} + +// ApiResultItem 接口返回值配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 20:23 2025/4/11 +type ApiResultItem struct { + Title string `json:"title" binding:"required,min=1,max=64" dc:"返回值标题" err:"返回值标题必须存在,且长度在[1,64]之间"` // 返回值标题 + DataPath string `json:"data_path" binding:"required,min=1,max=512" dc:"返回值路径" err:"返回值路径必须存在,且长度在[1,512]之间"` // 返回值路径 + DataLocation string `json:"data_location" binding:"required,oneof=BODY HEADER COOKIE" dc:"返回值位置" err:"返回值位置必须存在,其需要时一个有效的合法值"` // 返回值位置 + DataType string `json:"data_type" binding:"required,validate_data_type" dc:"返回值类型" err:"返回值类型必须存在,且需要是一个有效的合法值"` // 返回值类型 + DefaultValue string `json:"default_value" dc:"默认值"` // 默认值 + ExampleValue string `json:"example_value" dc:"示例值"` // 示例值 + Description string `json:"description" dc:"返回值描述"` // 返回值描述 +} diff --git a/swagger/parser.go b/swagger/parser.go index d72d1ca..61e434d 100644 --- a/swagger/parser.go +++ b/swagger/parser.go @@ -6,3 +6,643 @@ // // Date : 2025-04-11 20:07 package swagger + +import ( + "errors" + "fmt" + apiDocDefine "git.zhangdeman.cn/gateway/api-doc/define" + "git.zhangdeman.cn/zhangdeman/consts" + "git.zhangdeman.cn/zhangdeman/wrapper" + "net/http" + "sort" + "strings" +) + +// HandleOpenapiDocRes ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:00 2025/2/26 +func HandleOpenapiDocRes(docRes *apiDocDefine.OpenapiDoc, ignoreApiTable map[string]bool, dataField string) ([]*apiDocDefine.ApiConfig, error) { + if nil == ignoreApiTable { + ignoreApiTable = make(map[string]bool) + } + hod := &handleOpenapiDoc{ + docRes: docRes, + parseRes: make([]*apiDocDefine.ApiConfig, 0), + ignoreApiTable: ignoreApiTable, + dataField: dataField, + } + if err := hod.Parse(); nil != err { + return nil, err + } + return hod.parseRes, nil +} + +type handleOpenapiDoc struct { + docRes *apiDocDefine.OpenapiDoc + parseRes []*apiDocDefine.ApiConfig + ignoreApiTable map[string]bool + dataField string +} + +func (hod *handleOpenapiDoc) Parse() error { + // 排序, 保证输出顺序一致性 + uriList := make([]string, 0) + for uri := range hod.docRes.Paths { + uriList = append(uriList, uri) + } + sort.Strings(uriList) + for _, uri := range uriList { + itemPath := hod.docRes.Paths[uri] + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodGet, itemPath.Get); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodPut, itemPath.Put); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodPost, itemPath.Post); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodHead, itemPath.Head); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodPatch, itemPath.Patch); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodDelete, itemPath.Delete); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodConnect, itemPath.Connect); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodOptions, itemPath.Options); nil != err { + return err + } + if err := hod.apiPathConfigToProjectConfig(uri, http.MethodTrace, itemPath.Trace); nil != err { + return err + } + } + return nil +} + +// apiPathConfigToProjectConfig 解析请求方法 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:48 2025/2/26 +func (hod *handleOpenapiDoc) apiPathConfigToProjectConfig(uri string, method string, apiPathConfig *apiDocDefine.PathItemOperationConfig) error { + if hod.ignoreApiTable[fmt.Sprintf("%v_%v", strings.ToUpper(method), uri)] { + // 接口已存在 + return nil + } + if apiPathConfig == nil { + return nil + } + requestContentTypeList := make([]string, 0) + if nil != apiPathConfig.RequestBody { + for contentType := range apiPathConfig.RequestBody.Content { + requestContentTypeList = append(requestContentTypeList, contentType) + } + } + if len(requestContentTypeList) == 0 { + requestContentTypeList = append(requestContentTypeList, consts.MimeTypeXWWWFormUrlencoded) + } + sort.Strings(requestContentTypeList) + + tag := "未知分组" + if len(apiPathConfig.Tags) > 0 { + tag = apiPathConfig.Tags[0] + } + importUriConfig := &apiDocDefine.ApiConfig{ + Tag: tag, + Name: apiPathConfig.Summary, + Uri: uri, + Method: strings.ToUpper(method), + ContentType: requestContentTypeList[0], + Description: apiPathConfig.Description, + ParamList: make([]*apiDocDefine.ApiParamItem, 0), + ResultList: make([]*apiDocDefine.ApiResultItem, 0), + } + // 解析公共的 Parameters , 任意请求方法都可能有 // TODO: 数组&对象递归处理 + for _, itemParam := range apiPathConfig.Parameters { + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: itemParam.Name, + Name: itemParam.Name, + Location: hod.openapiDocLocation2GatewayLocation(itemParam.In), + ParamType: hod.openapiDocType2GatewayType(fmt.Sprintf("%v", itemParam.Schema.Type), itemParam.Schema.Format), + IsRequired: itemParam.Required, + DefaultValue: "∂", + ExampleValue: "", + Description: itemParam.Description, + }) + } + // post 类请求 body + if err := hod.handleRequestBody(importUriConfig, apiPathConfig.RequestBody, requestContentTypeList[0]); nil != err { + return err + } + // 处理response + if err := hod.handleResponseBody(importUriConfig, apiPathConfig.Responses); nil != err { + return err + } + // 解析response + hod.parseRes = append(hod.parseRes, importUriConfig) + return nil +} + +// handleRequestBody 解析request body +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 12:17 2025/2/27 +func (hod *handleOpenapiDoc) handleRequestBody(importUriConfig *apiDocDefine.ApiConfig, requestBodyData *apiDocDefine.RequestBody, selectRequestContentType string) error { + if nil == requestBodyData || nil == requestBodyData.Content { + return nil + } + if nil == requestBodyData.Content[selectRequestContentType] { + return fmt.Errorf("select content_type = %v is not found in requestBodyData.Content", selectRequestContentType) + } + requestBody := requestBodyData.Content[selectRequestContentType].Schema + if len(requestBody.Ref) == 0 { + return nil + } + requestBodyRequiredParamTable := map[string]bool{} + for _, itemParamName := range requestBody.Required { + requestBodyRequiredParamTable[itemParamName] = true + } + if len(requestBody.Ref) > 0 { + refCfg, err := hod.getResComponentsConfig(requestBody.Ref) + if nil != err { + return err + } + // ref 配置合并 + if nil == requestBodyData.Content[selectRequestContentType].Schema.Properties { + requestBodyData.Content[selectRequestContentType].Schema.Properties = make(map[string]*apiDocDefine.Property) + } + for paramName, itemParam := range refCfg.Properties { + requestBodyData.Content[selectRequestContentType].Schema.Properties[paramName] = itemParam + } + for _, itemParamName := range refCfg.Required { + requestBodyRequiredParamTable[itemParamName] = true + } + } + for requestBodyParamName, requestBodyParamConfig := range requestBody.Properties { + // 对象、数组、引用递归解析 + if err := hod.expendObjectOrArrayRequest(importUriConfig, requestBodyRequiredParamTable, "", "", requestBodyParamName, requestBodyParamConfig); nil != err { + return err + } + } + return nil +} + +func (hod *handleOpenapiDoc) expendObjectOrArrayRequest(importUriConfig *apiDocDefine.ApiConfig, requestBodyRequiredParamTable map[string]bool, rootRef string, parentPath string, currentPropertyName string, currentProperty *apiDocDefine.Property) error { + if len(parentPath) > 0 { + currentPropertyName = parentPath + "." + currentPropertyName + } + defaultValue := wrapper.AnyDataType(currentProperty.Default).ToString().Value() + // 基础数据类型 + if len(currentProperty.Properties) == 0 && currentProperty.Type != consts.SwaggerDataTypeObject && currentProperty.Type != consts.SwaggerDataTypeArray && len(currentProperty.Ref) == 0 { + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: currentPropertyName, + Name: currentPropertyName, + Location: consts.RequestDataLocationBody.String(), + ParamType: hod.openapiDocType2GatewayType(fmt.Sprintf("%v", currentProperty.Type), currentProperty.Format), + IsRequired: requestBodyRequiredParamTable[currentPropertyName], + DefaultValue: defaultValue, + ExampleValue: "", + Description: currentProperty.Description, + }) + return nil + } + // 处理数组(注意循环引用导致的递归死循环) + if currentProperty.Type == consts.SwaggerDataTypeArray { + if nil == currentProperty.Items { + if strings.HasSuffix(parentPath, ".[]") { + // 说明已经set过,不处理 + return nil + } + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: currentPropertyName, + Name: currentPropertyName, + Location: consts.RequestDataLocationBody.String(), + ParamType: consts.DataTypeSliceAny.String(), + IsRequired: requestBodyRequiredParamTable[currentPropertyName], + DefaultValue: "", + ExampleValue: "", + Description: currentProperty.Description, + }) + return nil + } + // 数组类型, 如果是基础类型的数组, 直接返回, map 数组或嵌套数组层层展开 + if currentProperty.Items.Type == consts.SwaggerDataTypeObject || currentProperty.Items.Type == consts.SwaggerDataTypeArray || len(currentProperty.Items.Ref) > 0 { + if !strings.HasSuffix(parentPath, ".[]") { + dataType := hod.openapiDocType2GatewayType(fmt.Sprintf("%v", currentProperty.Type), currentProperty.Format) + // 数组根key没设置过进行set + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: currentPropertyName, + Name: currentPropertyName, + Location: consts.RequestDataLocationBody.String(), + ParamType: "[]" + dataType, + IsRequired: requestBodyRequiredParamTable[currentPropertyName], + DefaultValue: defaultValue, + ExampleValue: "", + Description: currentProperty.Description, + }) + } + + if len(currentProperty.Items.Ref) == 0 { + return nil + } + // 解析ref内容 + refDefine, err := hod.getResComponentsConfig(currentProperty.Items.Ref) + if nil != err { + return err + } + if strings.Contains(rootRef, currentProperty.Items.Ref) { + dataType := consts.DataTypeAny.String() + // 循环引用 + if refDefine.Type == consts.SwaggerDataTypeObject { + dataType = consts.DataTypeSliceMapStringAny.String() + } else if refDefine.Type == consts.SwaggerDataTypeArray { + dataType = consts.DataTypeSliceSlice.String() + } + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: currentPropertyName, + Name: currentPropertyName, + Location: consts.RequestDataLocationBody.String(), + ParamType: dataType, + IsRequired: requestBodyRequiredParamTable[currentPropertyName], + DefaultValue: defaultValue, + ExampleValue: "", + Description: currentProperty.Description, + }) + return nil + } + // 数组子项是数组或者对象 + currentPropertyName = currentPropertyName + ".[]" + newRequiredTable := make(map[string]bool) + for _, item := range refDefine.Required { + newRequiredTable[item] = true + } + for itemName, itemVal := range refDefine.Properties { + if err = hod.expendObjectOrArrayRequest(importUriConfig, newRequiredTable, rootRef+currentProperty.Items.Ref, currentPropertyName, itemName, itemVal); nil != err { + return err + } + } + return nil + } else { + // 数组子项是基础类型 + dataType := hod.openapiDocType2GatewayType(fmt.Sprintf("%v", currentProperty.Type), currentProperty.Format) + if len(currentProperty.Format) == 0 { + // 未指定format, dataType 添加 [] 前缀 + dataType = "[]" + dataType + } + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: currentPropertyName, + Name: currentPropertyName, + Location: consts.RequestDataLocationBody.String(), + ParamType: dataType, + IsRequired: requestBodyRequiredParamTable[currentPropertyName], + DefaultValue: defaultValue, + ExampleValue: "", + Description: currentProperty.Description, + }) + return nil + } + } + if currentProperty.Type == consts.SwaggerDataTypeObject && currentProperty.Ref == "" && len(currentProperty.Properties) == 0 { + // 对象类型, 且未设置引用类型, 并且属性也为空, 对应any数据类型 + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: currentPropertyName, + Name: currentPropertyName, + Location: consts.RequestDataLocationBody.String(), + ParamType: consts.DataTypeAny.String(), + IsRequired: requestBodyRequiredParamTable[currentPropertyName], + DefaultValue: defaultValue, + ExampleValue: "", + Description: currentProperty.Description, + }) + return nil + } + // 遍历 Properties + for fieldName, fieldVal := range currentProperty.Properties { + importUriConfig.ParamList = append(importUriConfig.ParamList, &apiDocDefine.ApiParamItem{ + Title: fieldName, + Name: fieldName, + Location: consts.RequestDataLocationBody.String(), + ParamType: hod.openapiDocType2GatewayType(fmt.Sprintf("%v", fieldVal.Type), fieldVal.Format), + IsRequired: requestBodyRequiredParamTable[currentPropertyName], + DefaultValue: defaultValue, + Description: fieldVal.Description, + }) + // 对象或者数组, 深度递归 + if fieldVal.Type == consts.SwaggerDataTypeObject || fieldVal.Type == consts.SwaggerDataTypeArray { + if err := hod.expendObjectOrArrayRequest(importUriConfig, requestBodyRequiredParamTable, rootRef, currentPropertyName, fieldName, fieldVal); nil != err { + return err + } + } + if len(fieldVal.Ref) > 0 { + // 引用的结构体定义, 递归解析 + if propertyResDefine, err := hod.getResComponentsConfig(fieldVal.Ref); nil != err { + return err + } else { + newRequiredTable := make(map[string]bool) + for _, item := range propertyResDefine.Required { + newRequiredTable[item] = true + } + for _, itemProperty := range propertyResDefine.Properties { + if err = hod.expendObjectOrArrayRequest(importUriConfig, newRequiredTable, rootRef+fieldVal.Ref, currentPropertyName, fieldName, itemProperty); nil != err { + return err + } + } + } + } + } + return nil +} + +// handleResponseBody 解析响应body +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 13:44 2025/2/27 +func (hod *handleOpenapiDoc) handleResponseBody(importUriConfig *apiDocDefine.ApiConfig, requestBodyTable map[string]*apiDocDefine.Response) error { + if nil == requestBodyTable || nil == requestBodyTable["200"] { + return nil + } + if len(requestBodyTable["200"].Content) == 0 { + // 无返回值 + return nil + } + contentTypeList := []string{} + for itemContentType := range requestBodyTable["200"].Content { + contentTypeList = append(contentTypeList, itemContentType) + } + if len(contentTypeList) == 0 { + contentTypeList = append(contentTypeList, consts.MimeTypeJson) + } + sort.Strings(contentTypeList) + selectResponseContentType := contentTypeList[0] + responseBodyData := requestBodyTable["200"].Content[selectResponseContentType].Schema + // 处理properties + if hod.dataField == "" { + // 未指定数据字段 + for resultName, itemResult := range responseBodyData.Properties { + if err := hod.expendObjectOrArrayPath(importUriConfig, "", "", resultName, itemResult); nil != err { + return err + } + } + } else { + // 指定数据字段 + fieldConfig := responseBodyData.Properties[hod.dataField] + if nil == fieldConfig { + return fmt.Errorf("data_field=%v 在相应body中不存在, uri=%v", hod.dataField, importUriConfig.Uri) + } + for fieldName, fieldVal := range fieldConfig.Properties { + // 引用的结构体定义, 递归解析 + if err := hod.expendObjectOrArrayPath(importUriConfig, "", "", fieldName, fieldVal); nil != err { + return err + } + } + } + + if len(responseBodyData.Ref) == 0 { + return nil + } + refCfg, err := hod.getResComponentsConfig(responseBodyData.Ref) + if nil != err { + return err + } + // ref 配置合并 + for resultName, itemResult := range refCfg.Properties { + if err = hod.expendObjectOrArrayPath(importUriConfig, responseBodyData.Ref, "", resultName, itemResult); nil != err { + return err + } + } + return nil +} + +// expendObjectOrArrayPath 展开ref对象路径 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:28 2025/4/8 +func (hod *handleOpenapiDoc) expendObjectOrArrayPath(importUriConfig *apiDocDefine.ApiConfig, rootRef string, parentPath string, currentPropertyName string, currentProperty *apiDocDefine.Property) error { + if len(parentPath) > 0 { + currentPropertyName = parentPath + "." + currentPropertyName + } + defaultValue := wrapper.AnyDataType(currentProperty.Default).ToString().Value() + // 基础数据类型 + if len(currentProperty.Properties) == 0 && currentProperty.Type != consts.SwaggerDataTypeObject && currentProperty.Type != consts.SwaggerDataTypeArray && len(currentProperty.Ref) == 0 { + importUriConfig.ResultList = append(importUriConfig.ResultList, &apiDocDefine.ApiResultItem{ + Title: currentPropertyName, + DataPath: currentPropertyName, + DataLocation: consts.RequestDataLocationBody.String(), + DataType: hod.openapiDocType2GatewayType(fmt.Sprintf("%v", currentProperty.Type), currentProperty.Format), + DefaultValue: defaultValue, + ExampleValue: defaultValue, + Description: currentProperty.Description, + }) + return nil + } + // 处理数组(注意循环引用导致的递归死循环) + if currentProperty.Type == consts.SwaggerDataTypeArray { + if nil == currentProperty.Items { + if strings.HasSuffix(parentPath, ".[]") { + // 说明已经set过,不处理 + return nil + } + importUriConfig.ResultList = append(importUriConfig.ResultList, &apiDocDefine.ApiResultItem{ + Title: currentPropertyName, + DataPath: currentPropertyName, + DataLocation: consts.ResponseDataLocationBody.String(), + DataType: consts.DataTypeSliceAny.String(), + DefaultValue: defaultValue, + ExampleValue: defaultValue, + Description: currentProperty.Description, + }) + return nil + } + // 数组类型, 如果是基础类型的数组, 直接返回, map 数组或嵌套数组层层展开 + if currentProperty.Items.Type == consts.SwaggerDataTypeObject || currentProperty.Items.Type == consts.SwaggerDataTypeArray || len(currentProperty.Items.Ref) > 0 { + if !strings.HasSuffix(parentPath, ".[]") { + // 数组根key没设置过进行set + importUriConfig.ResultList = append(importUriConfig.ResultList, &apiDocDefine.ApiResultItem{ + Title: currentPropertyName, + DataPath: currentPropertyName, + DataLocation: consts.RequestDataLocationBody.String(), + DataType: consts.DataTypeSliceAny.String(), + DefaultValue: defaultValue, + ExampleValue: defaultValue, + Description: currentProperty.Description, + }) + } + + if len(currentProperty.Items.Ref) == 0 { + return nil + } + // 解析ref内容 + refDefine, err := hod.getResComponentsConfig(currentProperty.Items.Ref) + if nil != err { + return err + } + if strings.Contains(rootRef, currentProperty.Items.Ref) { + dataType := consts.DataTypeAny.String() + // 循环引用 + if refDefine.Type == consts.SwaggerDataTypeObject { + dataType = consts.DataTypeSliceMapStringAny.String() + } else if refDefine.Type == consts.SwaggerDataTypeArray { + dataType = consts.DataTypeSliceSlice.String() + } + importUriConfig.ResultList = append(importUriConfig.ResultList, &apiDocDefine.ApiResultItem{ + Title: currentPropertyName, + DataPath: currentPropertyName, + DataLocation: consts.RequestDataLocationBody.String(), + DataType: dataType, + DefaultValue: defaultValue, + ExampleValue: defaultValue, + Description: currentProperty.Description, + }) + return nil + } + // 数组子项是数组或者对象 + currentPropertyName = currentPropertyName + ".[]" + + for itemName, itemVal := range refDefine.Properties { + if err = hod.expendObjectOrArrayPath(importUriConfig, rootRef+currentProperty.Items.Ref, currentPropertyName, itemName, itemVal); nil != err { + return err + } + } + return nil + } else { + // 数组子项是基础类型 + dataType := hod.openapiDocType2GatewayType(fmt.Sprintf("%v", currentProperty.Type), currentProperty.Format) + if len(currentProperty.Format) == 0 { + // 未指定format, dataType 添加 [] 前缀 + dataType = "[]" + dataType + } + importUriConfig.ResultList = append(importUriConfig.ResultList, &apiDocDefine.ApiResultItem{ + Title: currentPropertyName, + DataPath: currentPropertyName, + DataLocation: consts.RequestDataLocationBody.String(), + DataType: dataType, + DefaultValue: defaultValue, + ExampleValue: defaultValue, + Description: currentProperty.Description, + }) + return nil + } + } + if currentProperty.Type == consts.SwaggerDataTypeObject && currentProperty.Ref == "" && len(currentProperty.Properties) == 0 { + // 对象类型, 且未设置引用类型, 并且属性也为空, 对应any数据类型 + importUriConfig.ResultList = append(importUriConfig.ResultList, &apiDocDefine.ApiResultItem{ + Title: currentPropertyName, + DataPath: currentPropertyName, + DataLocation: consts.RequestDataLocationBody.String(), + DataType: consts.DataTypeAny.String(), + DefaultValue: defaultValue, + ExampleValue: defaultValue, + Description: currentProperty.Description, + }) + return nil + } + // 遍历 Properties + for fieldName, fieldVal := range currentProperty.Properties { + importUriConfig.ResultList = append(importUriConfig.ResultList, &apiDocDefine.ApiResultItem{ + Title: fieldName, + DataPath: fieldName, + DataLocation: consts.RequestDataLocationBody.String(), + DataType: hod.openapiDocType2GatewayType(fmt.Sprintf("%v", fieldVal.Type), fieldVal.Format), + Description: fieldVal.Description, + DefaultValue: defaultValue, + ExampleValue: defaultValue, + }) + // 对象或者数组, 深度递归 + if fieldVal.Type == consts.SwaggerDataTypeObject || fieldVal.Type == consts.SwaggerDataTypeArray { + if err := hod.expendObjectOrArrayPath(importUriConfig, rootRef, currentPropertyName, fieldName, fieldVal); nil != err { + return err + } + } + if len(fieldVal.Ref) > 0 { + // 引用的结构体定义, 递归解析 + if propertyResDefine, err := hod.getResComponentsConfig(fieldVal.Ref); nil != err { + return err + } else { + for _, itemProperty := range propertyResDefine.Properties { + if err = hod.expendObjectOrArrayPath(importUriConfig, rootRef+fieldVal.Ref, currentPropertyName, fieldName, itemProperty); nil != err { + return err + } + } + } + } + } + return nil +} + +// getResComponentsConfig 获取ref指向components的定义 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 11:17 2025/2/27 +func (hod *handleOpenapiDoc) getResComponentsConfig(ref string) (*apiDocDefine.Schema, error) { + refKey := strings.TrimPrefix(ref, "#/components/schemas/") + if _, exist := hod.docRes.Components.Schemas[refKey]; exist { + return hod.docRes.Components.Schemas[refKey], nil + } + // apifox 导出的文档, ref 部分, 空格被转义, 但是 components 定义中空格未被转义 + refKey = strings.ReplaceAll(refKey, "%20", " ") + if _, exist := hod.docRes.Components.Schemas[refKey]; exist { + return hod.docRes.Components.Schemas[refKey], nil + } + return nil, errors.New("components not found : " + refKey) +} + +// openapiDocLocation2GatewayLocation 文档数据位置转为网关的数据位置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:06 2025/2/26 +func (hod *handleOpenapiDoc) openapiDocLocation2GatewayLocation(openapiDocLocation string) string { + openapiDocLocation = strings.ToUpper(openapiDocLocation) + for _, itemLocation := range consts.RequestDataLocationList { + if strings.ToUpper(itemLocation.Value.String()) == openapiDocLocation { + return itemLocation.Value.String() + } + } + // 没有明确配置参数位置:query/header/cookie/body等, 会自动按照请求尝试获取 + return consts.RequestDataLocationAny.String() +} + +// openapiDocType2GatewayType 文档数据类型转为网关数据类型 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:07 2025/2/26 +func (hod *handleOpenapiDoc) openapiDocType2GatewayType(openapiDocType string, formatType string) string { + openapiDocType = strings.ToLower(openapiDocType) + if formatType == "" { + formatType = openapiDocType + } + formatType = strings.ReplaceAll(strings.ReplaceAll(formatType, " ", ""), "interface{}", "any") + for _, itemType := range consts.DataTypeList { + if itemType.Value.String() == formatType { + return itemType.Value.String() + } + } + switch openapiDocType { + case consts.SwaggerDataTypeString: + return consts.DataTypeString.String() + case consts.SwaggerDataTypeInteger: + return consts.DataTypeInt.String() + case consts.SwaggerDataTypeNumber: + return consts.DataTypeFloat64.String() + case consts.SwaggerDataTypeArray: + return consts.DataTypeSliceAny.String() + case consts.SwaggerDataTypeObject: + return consts.DataTypeMapStrAny.String() + case consts.SwaggerDataTypeBoolean: + return consts.DataTypeBool.String() + } + // 兜底数据类型any + return consts.DataTypeAny.String() +}