// Package swagger ... // // Description : swagger ... // // Author : go_developer@163.com<白茶清欢> // // 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() }