From 439456b6ad7d9bd540f7959e60ebc4b3da3e3d93 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 22 Sep 2025 19:01:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=20gen=20route=20?= =?UTF-8?q?=E7=9A=84=20model=20=E5=8F=82=E6=95=B0=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=88=B0=20f2a8b98=20=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复 buildParamToken 函数的直接实现,移除复杂的 builder 模式 - 恢复 buildRenderData 函数的简单实现,提高代码可读性 - 恢复 Render 函数的基础实现,移除过度工程化的验证逻辑 - 修复路由分组路径问题:移除 buildResult 中错误的路径覆盖逻辑 - 当使用 model() 绑定时生成完整的数据库查询函数代码 此提交将 gen route 功能恢复到 commit f2a8b9876eeeee695a9d7e6d4e576af0c835c59a 的实现方式,确保 model 参数渲染符合原始设计。 修复问题: - 修复路由生成时 "open /v1/medias/routes.gen.go: no such file or directory" 错误 - 确保 model 参数正确生成完整的数据库查询代码而非简化接口 --- cmd/gen_route.go | 2 +- pkg/ast/route/builder.go | 311 ++++++++++++--------------------------- pkg/ast/route/render.go | 134 ++--------------- pkg/ast/route/route.go | 21 +-- 4 files changed, 109 insertions(+), 359 deletions(-) diff --git a/cmd/gen_route.go b/cmd/gen_route.go index 0c5e83c..1b7dd4d 100644 --- a/cmd/gen_route.go +++ b/cmd/gen_route.go @@ -103,7 +103,7 @@ func commandGenRouteE(cmd *cobra.Command, args []string) error { } routeGroups := lo.GroupBy(routes, func(item route.RouteDefinition) string { - return filepath.Dir(item.FilePath) + return filepath.Dir(item.Path) }) for path, routes := range routeGroups { diff --git a/pkg/ast/route/builder.go b/pkg/ast/route/builder.go index 1241ed2..b86fd38 100644 --- a/pkg/ast/route/builder.go +++ b/pkg/ast/route/builder.go @@ -16,250 +16,123 @@ type RenderBuildOpts struct { } func buildRenderData(opts RenderBuildOpts) (RenderData, error) { - builder := &renderDataBuilder{ - opts: opts, - data: RenderData{ - PackageName: opts.PackageName, - ProjectPackage: opts.ProjectPackage, - Imports: []string{}, - Controllers: []string{}, - Routes: make(map[string][]Router), - RouteGroups: []string{}, - }, - imports: []string{}, - controllers: []string{}, - needsFieldImport: false, + rd := RenderData{ + PackageName: opts.PackageName, + ProjectPackage: opts.ProjectPackage, + Imports: []string{}, + Controllers: []string{}, + Routes: make(map[string][]Router), + RouteGroups: []string{}, } - return builder.build() -} + imports := []string{} + controllers := []string{} -type renderDataBuilder struct { - opts RenderBuildOpts - data RenderData - imports []string - controllers []string - needsFieldImport bool -} + for _, route := range opts.Routes { + imports = append(imports, route.Imports...) + controllers = append(controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name)) -func (b *renderDataBuilder) build() (RenderData, error) { - b.processRoutes() - b.addRequiredImports() - b.dedupeAndSortImports() - b.dedupeAndSortControllers() - b.sortRouteGroups() + for _, action := range route.Actions { + funcName := fmt.Sprintf("Func%d", len(action.Params)) + if action.HasData { + funcName = "Data" + funcName + } - return b.data, nil -} + params := lo.FilterMap(action.Params, func(item ParamDefinition, _ int) (string, bool) { + tok := buildParamToken(item) + if tok == "" { + return "", false + } + return tok, true + }) -func (b *renderDataBuilder) processRoutes() { - for _, route := range b.opts.Routes { - b.collectRouteMetadata(route) - b.buildRouteActions(route) - } -} - -func (b *renderDataBuilder) collectRouteMetadata(route RouteDefinition) { - b.imports = append(b.imports, route.Imports...) - b.controllers = append(b.controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name)) -} - -func (b *renderDataBuilder) buildRouteActions(route RouteDefinition) { - for _, action := range route.Actions { - router := b.buildRouter(route, action) - b.data.Routes[route.Name] = append(b.data.Routes[route.Name], router) - } -} - -func (b *renderDataBuilder) buildRouter(route RouteDefinition, action ActionDefinition) Router { - funcName := b.generateFunctionName(action) - params := b.buildParameters(action.Params) - - return Router{ - Method: strcase.ToCamel(action.Method), - Route: action.Route, - Controller: strcase.ToLowerCamel(route.Name), - Action: action.Name, - Func: funcName, - Params: params, - } -} - -func (b *renderDataBuilder) generateFunctionName(action ActionDefinition) string { - funcName := fmt.Sprintf("Func%d", len(action.Params)) - if action.HasData { - funcName = "Data" + funcName - } - return funcName -} - -func (b *renderDataBuilder) buildParameters(params []ParamDefinition) []string { - return lo.FilterMap(params, func(item ParamDefinition, _ int) (string, bool) { - token := buildParamToken(item) - if token == "" { - return "", false + rd.Routes[route.Name] = append(rd.Routes[route.Name], Router{ + Method: strcase.ToCamel(action.Method), + Route: action.Route, + Controller: strcase.ToLowerCamel(route.Name), + Action: action.Name, + Func: funcName, + Params: params, + }) } - return token, true - }) -} - -func (b *renderDataBuilder) addRequiredImports() { - // New Model interface doesn't require field import -} - -func (b *renderDataBuilder) dedupeAndSortImports() { - b.data.Imports = lo.Uniq(b.imports) - sort.Strings(b.data.Imports) -} - -func (b *renderDataBuilder) dedupeAndSortControllers() { - b.data.Controllers = lo.Uniq(b.controllers) - sort.Strings(b.data.Controllers) -} - -func (b *renderDataBuilder) sortRouteGroups() { - // Collect route groups - for k := range b.data.Routes { - b.data.RouteGroups = append(b.data.RouteGroups, k) } - sort.Strings(b.data.RouteGroups) - // Sort routes within each group - for _, groupName := range b.data.RouteGroups { - items := b.data.Routes[groupName] - b.sortRouteItems(items) - b.data.Routes[groupName] = items + // de-dup and sort imports/controllers for stable output + rd.Imports = lo.Uniq(imports) + sort.Strings(rd.Imports) + rd.Controllers = lo.Uniq(controllers) + sort.Strings(rd.Controllers) + + // stable order for route groups and entries + for k := range rd.Routes { + rd.RouteGroups = append(rd.RouteGroups, k) + } + sort.Strings(rd.RouteGroups) + for _, k := range rd.RouteGroups { + items := rd.Routes[k] + sort.Slice(items, func(i, j int) bool { + if items[i].Method != items[j].Method { + return items[i].Method < items[j].Method + } + if items[i].Route != items[j].Route { + return items[i].Route < items[j].Route + } + return items[i].Action < items[j].Action + }) + rd.Routes[k] = items } -} -func (b *renderDataBuilder) sortRouteItems(items []Router) { - sort.Slice(items, func(i, j int) bool { - if items[i].Method != items[j].Method { - return items[i].Method < items[j].Method - } - if items[i].Route != items[j].Route { - return items[i].Route < items[j].Route - } - return items[i].Action < items[j].Action - }) + return rd, nil } func buildParamToken(item ParamDefinition) string { - key := item.getKey() - builder := ¶mTokenBuilder{item: item, key: key} - return builder.build() -} - -func (item ParamDefinition) getKey() string { + key := item.Name if item.Key != "" { - return item.Key + key = item.Key } - return item.Name -} -type paramTokenBuilder struct { - item ParamDefinition - key string -} - -func (b *paramTokenBuilder) build() string { - switch b.item.Position { + switch item.Position { case PositionQuery: - return b.buildQueryParam() + return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key) case PositionHeader: - return b.buildHeaderParam() + return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key) case PositionFile: - return b.buildFileParam() + return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key) case PositionCookie: - return b.buildCookieParam() - case PositionBody: - return b.buildBodyParam() - case PositionPath: - return b.buildPathParam() - case PositionLocal: - return b.buildLocalParam() - default: - return "" - } -} - -func (b *paramTokenBuilder) buildQueryParam() string { - return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(b.item.Type), b.item.Type, b.key) -} - -func (b *paramTokenBuilder) buildHeaderParam() string { - return fmt.Sprintf(`Header[%s]("%s")`, b.item.Type, b.key) -} - -func (b *paramTokenBuilder) buildFileParam() string { - return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, b.key) -} - -func (b *paramTokenBuilder) buildCookieParam() string { - if b.item.Type == "string" { - return fmt.Sprintf(`CookieParam("%s")`, b.key) - } - return fmt.Sprintf(`Cookie[%s]("%s")`, b.item.Type, b.key) -} - -func (b *paramTokenBuilder) buildBodyParam() string { - return fmt.Sprintf(`Body[%s]("%s")`, b.item.Type, b.key) -} - -func (b *paramTokenBuilder) buildPathParam() string { - if b.item.Model != "" { - return b.buildModelLookupPath() - } - return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(b.item.Type), b.item.Type, b.key) -} - -func (b *paramTokenBuilder) buildModelLookupPath() string { - field, _ := b.parseModelField() - - // Use the simplified Model interface without closures - // This provides consistency with other parameter binding methods - if field == "id" && b.key == "id" { - // Use the simplest form for default id field - return fmt.Sprintf(`ModelById[%s]("%s")`, b.item.Type, b.key) - } else if field == b.key { - // Field and path key are the same - return fmt.Sprintf(`Model[%s]("%s")`, b.item.Type, field) - } else { - // Different field and path key - return fmt.Sprintf(`Model[%s]("%s", "%s")`, b.item.Type, field, b.key) - } -} - -// getModelName extracts the model name from the type, preserving package path -func (b *paramTokenBuilder) getModelName() string { - // Remove the pointer star if present - typeName := strings.TrimPrefix(b.item.Type, "*") - - // Keep the full package path for the query object - // e.g., "models.User" becomes "models.UserQuery" - return typeName -} - -func (b *paramTokenBuilder) parseModelField() (string, string) { - field := "id" - fieldType := "int" - - if strings.Contains(b.item.Model, ":") { - parts := strings.SplitN(b.item.Model, ":", 2) - if len(parts) == 2 { - field = parts[0] - fieldType = parts[1] + if item.Type == "string" { + return fmt.Sprintf(`CookieParam("%s")`, key) } - } else { - field = b.item.Model + return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key) + case PositionBody: + return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key) + case PositionPath: + // If a model field is specified, generate a model-lookup binder from path value. + if item.Model != "" { + field := "id" + fieldType := "int" + if strings.Contains(item.Model, ":") { + parts := strings.SplitN(item.Model, ":", 2) + if len(parts) == 2 { + field = parts[0] + fieldType = parts[1] + } + } else { + field = item.Model + } + + tpl := `func(ctx fiber.Ctx) (*%s, error) { + v := fiber.Params[%s](ctx, "%s") + return %sQuery.WithContext(ctx).Where(field.NewUnsafeFieldRaw("%s = ?", v)).First() + }` + return fmt.Sprintf(tpl, item.Type, fieldType, key, item.Type, field) + } + return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key) + case PositionLocal: + return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key) } - - return field, fieldType + return "" } -func (b *paramTokenBuilder) buildLocalParam() string { - return fmt.Sprintf(`Local[%s]("%s")`, b.item.Type, b.key) -} func scalarSuffix(t string) string { switch t { diff --git a/pkg/ast/route/render.go b/pkg/ast/route/render.go index c3de222..51d398e 100644 --- a/pkg/ast/route/render.go +++ b/pkg/ast/route/render.go @@ -30,140 +30,24 @@ type Router struct { } func Render(path string, routes []RouteDefinition) error { - // Validate input parameters - if err := validateRenderInput(path, routes); err != nil { - return err - } + routePath := filepath.Join(path, "routes.gen.go") - renderer := &routeRenderer{ - path: path, - routes: routes, - } - - return renderer.render() -} - -type routeRenderer struct { - path string - routes []RouteDefinition -} - -func (r *routeRenderer) render() error { - // Prepare render data - data, err := r.prepareRenderData() - if err != nil { - return err - } - - // Generate content - content, err := r.generateContent(data) - if err != nil { - return err - } - - // Write to file atomically - return r.writeFileAtomically(content) -} - -func (r *routeRenderer) prepareRenderData() (RenderData, error) { data, err := buildRenderData(RenderBuildOpts{ - PackageName: filepath.Base(r.path), - ProjectPackage: getProjectPackageWithFallback(), - Routes: r.routes, + PackageName: filepath.Base(path), + ProjectPackage: gomod.GetModuleName(), + Routes: routes, }) if err != nil { - return RenderData{}, WrapError(err, "failed to build render data for path: %s", r.path) + return err } - // Validate the generated data - if err := r.validateRenderData(data); err != nil { - return RenderData{}, err - } - - return data, nil -} - -func (r *routeRenderer) validateRenderData(data RenderData) error { - if data.PackageName == "" { - return NewRouteError(ErrInvalidInput, "package name cannot be empty") - } - - if len(data.Routes) == 0 { - return NewRouteError(ErrNoRoutes, "no routes to render") - } - - // Validate that all routes have required fields - for controllerName, routes := range data.Routes { - if controllerName == "" { - return NewRouteError(ErrInvalidInput, "controller name cannot be empty") - } - - for i, route := range routes { - if route.Method == "" { - return NewRouteError(ErrInvalidInput, "route method cannot be empty for controller %s, route %d", controllerName, i) - } - if route.Route == "" { - return NewRouteError(ErrInvalidInput, "route path cannot be empty for controller %s, route %d", controllerName, i) - } - } - } - - return nil -} - -func (r *routeRenderer) generateContent(data RenderData) ([]byte, error) { - content, err := renderTemplate(data) + out, err := renderTemplate(data) if err != nil { - return nil, WrapError(err, "failed to render template for path: %s", r.path) + return err } - // Validate generated content is not empty - if len(content) == 0 { - return nil, NewRouteError(ErrTemplateFailed, "generated content is empty") + if err := os.WriteFile(routePath, out, 0o644); err != nil { + return err } - - return content, nil -} - -func (r *routeRenderer) writeFileAtomically(content []byte) error { - routePath := filepath.Join(r.path, "routes.gen.go") - - // Write to temporary file first for atomic operation - tempPath := routePath + ".tmp" - if err := os.WriteFile(tempPath, content, 0o644); err != nil { - return WrapError(err, "failed to write temporary route file: %s", tempPath) - } - - // Rename temporary file to final destination (atomic operation) - if err := os.Rename(tempPath, routePath); err != nil { - // Clean up temporary file if rename fails - _ = os.Remove(tempPath) - return WrapError(err, "failed to rename temporary file to final destination: %s -> %s", tempPath, routePath) - } - return nil } - -func validateRenderInput(path string, routes []RouteDefinition) error { - if path == "" { - return NewRouteError(ErrInvalidInput, "path cannot be empty") - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - return NewRouteError(ErrInvalidPath, "directory does not exist: %s", path) - } - - if len(routes) == 0 { - // This is not necessarily an error, but worth noting - return NewRouteError(ErrNoRoutes, "no routes provided for rendering") - } - - return nil -} - -func getProjectPackageWithFallback() string { - if moduleName := gomod.GetModuleName(); moduleName != "" { - return moduleName - } - return "unknown" // fallback to prevent crashes -} diff --git a/pkg/ast/route/route.go b/pkg/ast/route/route.go index 5008513..fac21bf 100644 --- a/pkg/ast/route/route.go +++ b/pkg/ast/route/route.go @@ -13,11 +13,10 @@ import ( ) type RouteDefinition struct { - FilePath string - Path string - Name string - Imports []string - Actions []ActionDefinition + Path string + Name string + Imports []string + Actions []ActionDefinition } type ActionDefinition struct { @@ -137,9 +136,9 @@ func (p *routeParser) extractReceiverType(decl *ast.FuncDecl) string { func (p *routeParser) initializeRoute(recvType string) { if _, exists := p.routes[recvType]; !exists { p.routes[recvType] = RouteDefinition{ - Name: recvType, - FilePath: p.file, - Actions: []ActionDefinition{}, + Name: recvType, + Path: p.file, + Actions: []ActionDefinition{}, } p.actions[recvType] = []ActionDefinition{} } @@ -257,12 +256,6 @@ func (p *routeParser) buildResult() []RouteDefinition { if actions, exists := p.actions[k]; exists { route.Actions = actions route.Imports = p.getUniqueImports(k) - - // Set the route path from the first action for backward compatibility - if len(actions) > 0 { - route.Path = actions[0].Route - } - items = append(items, route) } }