From 12faa04a7edecb064bd38ebd90c60abc07a01675 Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 17 Dec 2025 23:42:40 +0800 Subject: [PATCH] fix: route issues --- pkg/ast/route/builder.go | 4 +- pkg/ast/route/manual.go.tpl | 9 ++ pkg/ast/route/render.go | 16 +++ pkg/ast/route/renderer.go | 84 +++++++++++++- pkg/ast/route/router.go.tpl | 6 +- templates/project/llm.txt.raw | 207 ++++++++++++++++++++++++++++++++++ 6 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 pkg/ast/route/manual.go.tpl create mode 100644 templates/project/llm.txt.raw diff --git a/pkg/ast/route/builder.go b/pkg/ast/route/builder.go index 49a216b..e697cbc 100644 --- a/pkg/ast/route/builder.go +++ b/pkg/ast/route/builder.go @@ -7,10 +7,12 @@ import ( "github.com/iancoleman/strcase" "github.com/samber/lo" + "go.ipao.vip/atomctl/v2/pkg/utils/gomod" ) type RenderBuildOpts struct { PackageName string + ModuleName string ProjectPackage string Routes []RouteDefinition } @@ -19,6 +21,7 @@ func buildRenderData(opts RenderBuildOpts) (RenderData, error) { rd := RenderData{ PackageName: opts.PackageName, ProjectPackage: opts.ProjectPackage, + ModuleName: gomod.GetModuleName(), Imports: []string{}, Controllers: []string{}, Routes: make(map[string][]Router), @@ -147,7 +150,6 @@ func buildParamToken(item ParamDefinition) string { return "" } - func scalarSuffix(t string) string { switch t { case "string", "int", "int8", "int16", "int32", "int64", diff --git a/pkg/ast/route/manual.go.tpl b/pkg/ast/route/manual.go.tpl new file mode 100644 index 0000000..1726c52 --- /dev/null +++ b/pkg/ast/route/manual.go.tpl @@ -0,0 +1,9 @@ +package {{.PackageName}} + +func (r *Routes) Path() string { + return "/{{.PackageName}}" +} + +func (r *Routes) Middlewares() []any{ + return []any{} +} diff --git a/pkg/ast/route/render.go b/pkg/ast/route/render.go index 51d398e..511718a 100644 --- a/pkg/ast/route/render.go +++ b/pkg/ast/route/render.go @@ -11,8 +11,12 @@ import ( //go:embed router.go.tpl var routeTpl string +//go:embed manual.go.tpl +var routeManualTpl string + type RenderData struct { PackageName string + ModuleName string ProjectPackage string Imports []string Controllers []string @@ -31,9 +35,11 @@ type Router struct { func Render(path string, routes []RouteDefinition) error { routePath := filepath.Join(path, "routes.gen.go") + routeManualPath := filepath.Join(path, "routes.manual.go") data, err := buildRenderData(RenderBuildOpts{ PackageName: filepath.Base(path), + ModuleName: gomod.GetModuleName(), ProjectPackage: gomod.GetModuleName(), Routes: routes, }) @@ -49,5 +55,15 @@ func Render(path string, routes []RouteDefinition) error { if err := os.WriteFile(routePath, out, 0o644); err != nil { return err } + // if routes.manual.go not exists then create it + if _, err := os.Stat(routeManualPath); os.IsNotExist(err) { + manualOut, err := renderManualTemplate(data) + if err != nil { + return err + } + if err := os.WriteFile(routeManualPath, manualOut, 0o644); err != nil { + return err + } + } return nil } diff --git a/pkg/ast/route/renderer.go b/pkg/ast/route/renderer.go index c354c56..de03322 100644 --- a/pkg/ast/route/renderer.go +++ b/pkg/ast/route/renderer.go @@ -26,9 +26,10 @@ type TemplateInfo struct { // RouteRenderer implements TemplateRenderer for route generation type RouteRenderer struct { - template *template.Template - info TemplateInfo - logger *log.Entry + template *template.Template + manualTemplate *template.Template + info TemplateInfo + logger *log.Entry } // NewRouteRenderer creates a new RouteRenderer instance with proper initialization @@ -54,6 +55,10 @@ func NewRouteRenderer() *RouteRenderer { renderer.logger.WithError(err).Error("Failed to initialize template") return nil } + if err := renderer.initializeManualTemplate(); err != nil { + renderer.logger.WithError(err).Error("Failed to initialize manual template") + return nil + } renderer.info.Size = len(routeTpl) renderer.logger.WithFields(log.Fields{ @@ -64,6 +69,22 @@ func NewRouteRenderer() *RouteRenderer { return renderer } +func (r *RouteRenderer) initializeManualTemplate() error { + // Create template with sprig functions and custom options + tmpl := template.New(r.info.Name + "manual"). + Funcs(sprig.FuncMap()). + Option("missingkey=error") + + // Parse the template + parsedTmpl, err := tmpl.Parse(routeManualTpl) + if err != nil { + return WrapError(err, "failed to parse route template") + } + + r.manualTemplate = parsedTmpl + return nil +} + // initializeTemplate sets up the template with proper functions and options func (r *RouteRenderer) initializeTemplate() error { // Create template with sprig functions and custom options @@ -81,6 +102,41 @@ func (r *RouteRenderer) initializeTemplate() error { return nil } +// Render renders the template with the provided data +func (r *RouteRenderer) RenderManual(data RenderData) ([]byte, error) { + // Validate input data + if err := r.validateRenderData(data); err != nil { + return nil, err + } + + // Create buffer for rendering + var buf bytes.Buffer + buf.Grow(estimatedBufferSize(data)) // Pre-allocate buffer for better performance + + // Execute template with error handling + if err := r.manualTemplate.Execute(&buf, data); err != nil { + r.logger.WithError(err).WithFields(log.Fields{ + "package_name": data.PackageName, + "routes_count": len(data.Routes), + }).Error("Template execution failed") + return nil, WrapError(err, "template execution failed for package: %s", data.PackageName) + } + + // Validate rendered content + result := buf.Bytes() + if len(result) == 0 { + return nil, NewRouteError(ErrTemplateFailed, "rendered content is empty for package: %s", data.PackageName) + } + + r.logger.WithFields(log.Fields{ + "package_name": data.PackageName, + "routes_count": len(data.Routes), + "content_length": len(result), + }).Debug("Template rendered successfully") + + return result, nil +} + // Render renders the template with the provided data func (r *RouteRenderer) Render(data RenderData) ([]byte, error) { // Validate input data @@ -152,10 +208,20 @@ func (r *RouteRenderer) validateRenderData(data RenderData) error { for i, route := range routes { if route.Method == "" { - return NewRouteError(ErrInvalidInput, "route method cannot be empty for controller %s, route %d", controllerName, i) + 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 NewRouteError( + ErrInvalidInput, + "route path cannot be empty for controller %s, route %d", + controllerName, + i, + ) } } } @@ -189,3 +255,11 @@ func renderTemplate(data RenderData) ([]byte, error) { } return renderer.Render(data) } + +func renderManualTemplate(data RenderData) ([]byte, error) { + renderer := NewRouteRenderer() + if renderer == nil { + return nil, NewRouteError(ErrTemplateFailed, "failed to create route renderer") + } + return renderer.RenderManual(data) +} diff --git a/pkg/ast/route/router.go.tpl b/pkg/ast/route/router.go.tpl index 6d3009c..3736e21 100644 --- a/pkg/ast/route/router.go.tpl +++ b/pkg/ast/route/router.go.tpl @@ -10,6 +10,8 @@ import ( {{.}} {{- end }} {{- end }} + "{{.ModuleName}}/app/middlewares" + . "go.ipao.vip/atom/fen" _ "go.ipao.vip/atom" _ "go.ipao.vip/atom/contracts" @@ -23,6 +25,8 @@ import ( // @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { log *log.Entry `inject:"false"` + middlewares *middlewares.Middlewares + {{- if .Controllers }} // Controller instances {{- range .Controllers }} @@ -54,7 +58,7 @@ func (r *Routes) Register(router fiber.Router) { {{- range $value }} {{- if .Route }} r.log.Debugf("Registering route: {{.Method}} {{.Route}} -> {{.Controller}}.{{.Action}}") - router.{{.Method}}("{{.Route}}", {{.Func}}( + router.{{.Method}}("{{.Route}}"[len(r.Path()):], {{.Func}}( r.{{.Controller}}.{{.Action}}, {{- if .Params }} {{- range .Params }} diff --git a/templates/project/llm.txt.raw b/templates/project/llm.txt.raw new file mode 100644 index 0000000..6d69a38 --- /dev/null +++ b/templates/project/llm.txt.raw @@ -0,0 +1,207 @@ +# Backend Dev Rules (HTTP API + Model) + +This file condenses `backend/docs/dev/http_api.md` + `backend/docs/dev/model.md` into a checklist/rule format for LLMs. + +--- + +## 0) Golden rules (DO / DO NOT) + +- DO follow existing module layout under `backend/app/http//`. +- DO keep controller methods thin: parse/bind → call `services.*` → return result/error. +- DO regenerate code after changes (routes/docs/models). +- DO NOT manually edit generated files: +- `backend/app/http/**/routes.gen.go` +- `backend/app/http/**/provider.gen.go` +- `backend/docs/docs.go` +- DO keep Swagger annotations consistent with actual Fiber route paths (including `:param`). + +--- + +## 1) Add a new HTTP API endpoint + +### 1.1 Where code lives + +- Controllers: `backend/app/http//*.go` +- Example module: `backend/app/http/super/tenant.go`, `backend/app/http/super/user.go` +- DTOs: `backend/app/http//dto/*` +- Routes (generated): `backend/app/http//routes.gen.go` +- Swagger output (generated): `backend/docs/swagger.yaml`, `backend/docs/swagger.json`, `backend/docs/docs.go` + +### 1.2 Controller method signatures + +- “Return data” endpoints: return `(, error)` +- Example: `(*requests.Pager, error)` for paginated list +- “No data” endpoints: return `error` + +### 1.3 Swagger annotations (minimum set) + +Place above the handler function: + +- `@Summary` +- `@Tags` +- `@Accept json` +- `@Produce json` +- `@Param` (query/path/body as needed) +- `@Success` for 200 responses +- `@Router [get|post|patch|delete|put]` +- `@Bind` for parameters (see below) + +Common `@Success` patterns: + +- Paginated list: `requests.Pager{items=dto.Item}` +- Single object: `dto.Item` +- Array: `{array} dto.Item` + +### 1.4 Parameter binding (@Bind) + +Format: + +`@Bind [key()] [model(|[:])]` + +Positions: + +- `path`, `query`, `body`, `header`, `cookie`, `local`, `file` + +Notes: + +- `paramName` MUST match function parameter name (case-sensitive). +- Default key name is `paramName` ; override via `key(...)`. +- Scalar types: `string/int/int32/int64/float32/float64/bool`. +- Pointer types are supported (framework will handle deref for most positions). + +#### Model binding (path-only) + +Used to bind a model instance from a path value: + +- `model(id)` (recommended) +- `model(id:int)` / `model(code:string)` +- `model(pkg.Type:field)` or `model(pkg.Type)` (default field is `id`) + +Behavior: + +- Generated binder queries by field and returns first row as the parameter value. +- Auto-imports field helper for query building. + +### 1.5 Generate routes + providers + swagger docs + +Run from `backend/`: + +- Generate routes: `atomctl gen route` +- Generate providers: `atomctl gen provider` +- Generate swagger docs: `atomctl swag init` + +### 1.6 Local verify + +- Build/run: `make run` +- Use REST client examples: `backend/test/[module]/[controller].http` (extend it for new endpoints) + +### 1.7 Testing + +- Prefer existing test style under `backend/tests/e2e`. +- Run: `make test` + +--- + +## 2) Add / update a DB model + +Models live in: + +- `backend/database/models/*` (generated model code + optional manual extensions) + +### 2.1 Migration → model generation workflow + +1) Create migration: + +- `atomctl migrate create alter_table` or `atomctl migrate create create_table` + +2) Edit migration: + +- No explicit `BEGIN/COMMIT` needed (framework handles). +- Table name should be plural (e.g. `tenants`). + +3) Apply migration: + +- `atomctl migrate up` + +4) Map complex field types (JSON/ARRAY/UUID/…) via transform file: + +- `backend/database/.transform.yaml` → `field_type.` + +5) Generate models: + +- `atomctl gen model` + +### 2.2 Enum strategy + +- DO NOT use native DB ENUM. +- Define enums in Go under `backend/pkg/consts/
.go`, example: + +```go +// swagger:enum UserStatus +// ENUM(pending_verify, verified, banned, ) +type UserStatus string +``` + +- Generate enum code: `atomctl gen enum` + +### 2.3 Supported field types (`gen/types/`) + +`backend/database/.transform.yaml` typically imports `go.ipao.vip/gen` so you can use `types.*` in `field_type`. + +Common types: + +- JSON: `types.JSON`, `types.JSONMap`, `types.JSONType[T]`, `types.JSONSlice[T]` +- Array: `types.Array[T]` +- UUID: `types.UUID`, `types.BinUUID` +- Date/Time: `types.Date`, `types.Time` +- Money/XML/URL/Binary: `types.Money`, `types.XML`, `types.URL`, `types.HexBytes` +- Bit string: `types.BitString` +- Network: `types.Inet`, `types.CIDR`, `types.MACAddr` +- Ranges: `types.Int4Range`, `types.Int8Range`, `types.NumRange`, `types.TsRange`, `types.TstzRange`, `types.DateRange` +- Geometry: `types.Point`, `types.Polygon`, `types.Box`, `types.Circle`, `types.Path` +- Fulltext: `types.TSQuery`, `types.TSVector` +- Nullable: `types.Null[T]` and aliases (requires DB NULL) + +Reference: + +- Detailed examples: `gen/types/README.md` + +### 2.4 Relationships (GORM-aligned) via `.transform.yaml` + +Define in `field_relate.
.`: + +- `relation`: `belongs_to` | `has_one` | `has_many` | `many_to_many` +- `table`: target table +- `pivot`: join table (many_to_many only) +- `foreign_key`, `references` +- `join_foreign_key`, `join_references` (many_to_many only) +- `json`: JSON field name in API outputs + +Generator will convert snake_case columns to Go struct field names (e.g. `class_id` → `ClassID`). + +### 2.5 Extending generated models + +- Add manual methods/hooks by creating `backend/database/models/
.go`. +- Keep generated files untouched ; put custom logic only in your own file(s). + +--- + +## 3) Service layer injection (when adding services) + +- Services are in `backend/app/services`. +- After creating/updating a service provider, regenerate wiring: + - `atomctl gen service` + - `atomctl gen provider` +- Service call conventions: + - **Service-to-service (inside `services` package)**: call directly as `CamelCaseServiceStructName.Method()` (no `services.` prefix). + - **From outside (controllers/handlers/etc.)**: call via the package entrypoint `services.CamelCaseServiceStructName.Method()`. + +--- + +## 4) Quick command summary (run in `backend/`) + +- `make run` / `make build` / `make test` +- `atomctl gen route` / `atomctl gen provider` / `atomctl swag init` +- `atomctl migrate create ...` / `atomctl migrate up` +- `atomctl gen model` / `atomctl gen enum` / `atomctl gen service` +- `make init` (full refresh)