package server import ( "encoding/json" "log/slog" "regexp" "sort" "strings" "sync" "kra/internal/routeinfo" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "github.com/swaggo/swag" ) const swaggerInstanceName = "kra-admin" var ( swaggerPathParameter = regexp.MustCompile(`:([A-Za-z0-9_]+)`) swaggerRegistration sync.Once swaggerDocument runtimeSwaggerDocument ) type runtimeSwaggerDocument struct { mu sync.RWMutex doc string } func (d *runtimeSwaggerDocument) ReadDoc() string { d.mu.RLock() defer d.mu.RUnlock() return d.doc } func (d *runtimeSwaggerDocument) replace(doc string) { d.mu.Lock() d.doc = doc d.mu.Unlock() } func registerSwagger(engine *gin.Engine, prefix, version string, logger *slog.Logger) { swaggerRegistration.Do(func() { swag.Register(swaggerInstanceName, &swaggerDocument) }) swaggerDocument.replace(buildSwaggerDocument(engine.Routes(), prefix, version)) path := strings.TrimSuffix(prefix, "/") + "/swagger/*any" engine.GET(path, ginSwagger.WrapHandler( swaggerFiles.Handler, ginSwagger.InstanceName(swaggerInstanceName), ginSwagger.URL("doc.json"), ginSwagger.PersistAuthorization(true), )) if logger != nil { logger.Info("register swagger handler", "mod", "system", "path", path) } } func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string { basePath := strings.TrimSuffix(prefix, "/") if basePath == "" { basePath = "/" } sort.Slice(routes, func(i, j int) bool { if routes[i].Path == routes[j].Path { return routes[i].Method < routes[j].Method } return routes[i].Path < routes[j].Path }) paths := make(map[string]map[string]any, len(routes)) for _, route := range routes { if strings.HasSuffix(route.Path, "/*filepath") { continue } method := strings.ToLower(route.Method) switch method { case "get", "post", "put", "delete", "patch": default: continue } apiPath := route.Path if prefix != "" { apiPath = strings.TrimPrefix(apiPath, strings.TrimSuffix(prefix, "/")) } if apiPath == "" { apiPath = "/" } documentPath := swaggerPathParameter.ReplaceAllString(apiPath, `{$1}`) group, description := routeinfo.Metadata(route.Method, apiPath) if description == "" { description = route.Method + " " + apiPath } operation := map[string]any{ "tags": []string{group}, "summary": description, "operationId": swaggerOperationID(route.Method, apiPath), "produces": []string{"application/json"}, "responses": map[string]any{ "200": map[string]any{"description": "OK", "schema": map[string]any{"$ref": "#/definitions/Response"}}, }, } if parameters := swaggerPathParameters(apiPath); len(parameters) > 0 { operation["parameters"] = parameters } if !swaggerPublicPath(apiPath) { operation["security"] = []map[string][]string{{"ApiKeyAuth": {}}} } if paths[documentPath] == nil { paths[documentPath] = map[string]any{} } paths[documentPath][method] = operation } document := map[string]any{ "swagger": "2.0", "info": map[string]any{"title": "Kra Administration API", "version": version}, "basePath": basePath, "schemes": []string{"http", "https"}, "consumes": []string{"application/json"}, "produces": []string{"application/json"}, "securityDefinitions": map[string]any{ "ApiKeyAuth": map[string]any{"type": "apiKey", "name": "x-token", "in": "header"}, }, "paths": paths, "definitions": map[string]any{ "Response": map[string]any{ "type": "object", "properties": map[string]any{ "code": map[string]any{"type": "integer"}, "data": map[string]any{"type": "object"}, "msg": map[string]any{"type": "string"}, }, }, }, } raw, _ := json.Marshal(document) return string(raw) } func swaggerOperationID(method, path string) string { value := strings.ToLower(method) + "_" + strings.Trim(path, "/") value = swaggerPathParameter.ReplaceAllString(value, "$1") return strings.Map(func(r rune) rune { if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' { return r } return '_' }, value) } func swaggerPathParameters(path string) []map[string]any { matches := swaggerPathParameter.FindAllStringSubmatch(path, -1) parameters := make([]map[string]any, 0, len(matches)) for _, match := range matches { parameters = append(parameters, map[string]any{"name": match[1], "in": "path", "required": true, "type": "string"}) } return parameters } func swaggerPublicPath(path string) bool { for _, marker := range []string{ "/health", "/base/login", "/base/captcha", "/init/checkdb", "/init/initdb", "/api/freshCasbin", "/sysExportTemplate/exportExcelByToken", "/sysExportTemplate/exportTemplateByToken", "/sysError/createSysError", "/info/getInfoDataSource", "/info/getInfoPublic", } { if path == marker { return true } } return false }