feat: Refactor AST generation routes workflow

- Introduced a comprehensive data model for route definitions, parameters, and validation rules.
- Established component interfaces for route parsing, comment parsing, import resolution, route building, validation, and rendering.
- Developed a detailed implementation plan outlining execution flow, user requirements, and compliance with design principles.
- Created a quickstart guide to assist users in utilizing the refactored system effectively.
- Conducted thorough research on existing architecture, identifying key improvements and establishing a refactoring strategy.
- Specified functional requirements and user scenarios to ensure clarity and testability.
- Generated a task list for implementation, emphasizing test-driven development and parallel execution where applicable.
This commit is contained in:
Rogee
2025-09-22 11:33:13 +08:00
parent 0cfc573960
commit 824861c27c
17 changed files with 3324 additions and 272 deletions

View File

@@ -16,131 +16,237 @@ type RenderBuildOpts struct {
}
func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
rd := RenderData{
PackageName: opts.PackageName,
ProjectPackage: opts.ProjectPackage,
Imports: []string{},
Controllers: []string{},
Routes: make(map[string][]Router),
RouteGroups: []string{},
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,
}
imports := []string{}
controllers := []string{}
// Track if any param uses model lookup, which requires the field package.
needsFieldImport := false
return builder.build()
}
for _, route := range opts.Routes {
imports = append(imports, route.Imports...)
controllers = append(controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name))
type renderDataBuilder struct {
opts RenderBuildOpts
data RenderData
imports []string
controllers []string
needsFieldImport bool
}
for _, action := range route.Actions {
funcName := fmt.Sprintf("Func%d", len(action.Params))
if action.HasData {
funcName = "Data" + funcName
}
func (b *renderDataBuilder) build() (RenderData, error) {
b.processRoutes()
b.addRequiredImports()
b.dedupeAndSortImports()
b.dedupeAndSortControllers()
b.sortRouteGroups()
params := lo.FilterMap(action.Params, func(item ParamDefinition, _ int) (string, bool) {
tok := buildParamToken(item)
if tok == "" {
return "", false
}
if item.Model != "" {
needsFieldImport = true
}
return tok, true
})
return b.data, nil
}
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,
})
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
}
}
if item.Model != "" {
b.needsFieldImport = true
}
return token, true
})
}
// Add field import if any model lookups are used
if needsFieldImport {
imports = append(imports, `field "go.ipao.vip/gen/field"`)
func (b *renderDataBuilder) addRequiredImports() {
if b.needsFieldImport {
b.imports = append(b.imports, `field "go.ipao.vip/gen/field"`)
}
}
// 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)
func (b *renderDataBuilder) dedupeAndSortImports() {
b.data.Imports = lo.Uniq(b.imports)
sort.Strings(b.data.Imports)
}
// 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) 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)
return rd, nil
// Sort routes within each group
for _, groupName := range b.data.RouteGroups {
items := b.data.Routes[groupName]
b.sortRouteItems(items)
b.data.Routes[groupName] = 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
})
}
func buildParamToken(item ParamDefinition) string {
key := item.Name
key := item.getKey()
builder := &paramTokenBuilder{item: item, key: key}
return builder.build()
}
func (item ParamDefinition) getKey() string {
if item.Key != "" {
key = item.Key
return item.Key
}
return item.Name
}
switch item.Position {
type paramTokenBuilder struct {
item ParamDefinition
key string
}
func (b *paramTokenBuilder) build() string {
switch b.item.Position {
case PositionQuery:
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
return b.buildQueryParam()
case PositionHeader:
return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key)
return b.buildHeaderParam()
case PositionFile:
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key)
return b.buildFileParam()
case PositionCookie:
if item.Type == "string" {
return fmt.Sprintf(`CookieParam("%s")`, key)
}
return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key)
return b.buildCookieParam()
case PositionBody:
return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key)
return b.buildBodyParam()
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)
return b.buildPathParam()
case PositionLocal:
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
return b.buildLocalParam()
default:
return ""
}
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, fieldType := b.parseModelField()
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, b.item.Type, fieldType, b.key, b.item.Type, field)
}
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]
}
} else {
field = b.item.Model
}
return field, fieldType
}
func (b *paramTokenBuilder) buildLocalParam() string {
return fmt.Sprintf(`Local[%s]("%s")`, b.item.Type, b.key)
}
func scalarSuffix(t string) string {

58
pkg/ast/route/errors.go Normal file
View File

@@ -0,0 +1,58 @@
package route
import "fmt"
// Error types for better error handling
type RouteErrorCode int
const (
ErrInvalidInput RouteErrorCode = iota
ErrInvalidPath
ErrNoRoutes
ErrParseFailed
ErrTemplateFailed
ErrFileWriteFailed
)
type RouteError struct {
Code RouteErrorCode
Message string
Cause error
}
func (e *RouteError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("route error [%d]: %s (cause: %v)", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("route error [%d]: %s", e.Code, e.Message)
}
func (e *RouteError) Unwrap() error {
return e.Cause
}
func (e *RouteError) WithCause(cause error) *RouteError {
e.Cause = cause
return e
}
func NewRouteError(code RouteErrorCode, format string, args ...interface{}) *RouteError {
return &RouteError{
Code: code,
Message: fmt.Sprintf(format, args...),
}
}
func WrapError(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
// If it's already a RouteError, just wrap it with more context
if routeErr, ok := err.(*RouteError); ok {
return NewRouteError(routeErr.Code, format, args...).WithCause(routeErr)
}
// Wrap other errors with a generic parse error
return NewRouteError(ErrParseFailed, format, args...).WithCause(err)
}

View File

@@ -8,6 +8,7 @@ import (
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
)
//go:embed router.go.tpl
var routeTpl string
@@ -30,24 +31,140 @@ type Router struct {
}
func Render(path string, routes []RouteDefinition) error {
routePath := filepath.Join(path, "routes.gen.go")
// Validate input parameters
if err := validateRenderInput(path, routes); err != nil {
return err
}
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(path),
ProjectPackage: gomod.GetModuleName(),
Routes: routes,
PackageName: filepath.Base(r.path),
ProjectPackage: getProjectPackageWithFallback(),
Routes: r.routes,
})
if err != nil {
return err
return RenderData{}, WrapError(err, "failed to build render data for path: %s", r.path)
}
out, err := renderTemplate(data)
if err != nil {
return err
// Validate the generated data
if err := r.validateRenderData(data); err != nil {
return RenderData{}, err
}
if err := os.WriteFile(routePath, out, 0o644); err != nil {
return 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)
if err != nil {
return nil, WrapError(err, "failed to render template for path: %s", r.path)
}
// Validate generated content is not empty
if len(content) == 0 {
return nil, NewRouteError(ErrTemplateFailed, "generated content is empty")
}
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
}

View File

@@ -5,18 +5,187 @@ import (
"text/template"
"github.com/Masterminds/sprig/v3"
log "github.com/sirupsen/logrus"
)
var routerTmpl = template.Must(template.New("route").
Funcs(sprig.FuncMap()).
Option("missingkey=error").
Parse(routeTpl),
)
// TemplateRenderer defines the interface for template rendering operations
type TemplateRenderer interface {
Render(data RenderData) ([]byte, error)
Validate() error
GetTemplateInfo() TemplateInfo
}
func renderTemplate(data RenderData) ([]byte, error) {
var buf bytes.Buffer
if err := routerTmpl.Execute(&buf, data); err != nil {
// TemplateInfo provides metadata about the template
type TemplateInfo struct {
Name string
Version string
Functions []string
Options []string
Size int
}
// RouteRenderer implements TemplateRenderer for route generation
type RouteRenderer struct {
template *template.Template
info TemplateInfo
logger *log.Entry
}
// NewRouteRenderer creates a new RouteRenderer instance with proper initialization
func NewRouteRenderer() *RouteRenderer {
renderer := &RouteRenderer{
logger: log.WithField("module", "route-renderer"),
info: TemplateInfo{
Name: "router",
Version: "1.0.0",
Functions: []string{
"sprig",
"template",
"custom",
},
Options: []string{
"missingkey=error",
},
},
}
// Initialize template with error handling
if err := renderer.initializeTemplate(); err != nil {
renderer.logger.WithError(err).Error("Failed to initialize template")
return nil
}
renderer.info.Size = len(routeTpl)
renderer.logger.WithFields(log.Fields{
"template_size": renderer.info.Size,
"version": renderer.info.Version,
}).Info("Route renderer initialized successfully")
return renderer
}
// initializeTemplate sets up the template with proper functions and options
func (r *RouteRenderer) initializeTemplate() error {
// Create template with sprig functions and custom options
tmpl := template.New(r.info.Name).
Funcs(sprig.FuncMap()).
Option("missingkey=error")
// Parse the template
parsedTmpl, err := tmpl.Parse(routeTpl)
if err != nil {
return WrapError(err, "failed to parse route template")
}
r.template = parsedTmpl
return nil
}
// Render renders the template with the provided data
func (r *RouteRenderer) Render(data RenderData) ([]byte, error) {
// Validate input data
if err := r.validateRenderData(data); err != nil {
return nil, err
}
return buf.Bytes(), nil
// 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.template.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
}
// Validate checks if the renderer is properly configured
func (r *RouteRenderer) Validate() error {
if r.template == nil {
return NewRouteError(ErrTemplateFailed, "template is not initialized")
}
if r.info.Name == "" {
return NewRouteError(ErrTemplateFailed, "template name is not set")
}
return nil
}
// GetTemplateInfo returns metadata about the template
func (r *RouteRenderer) GetTemplateInfo() TemplateInfo {
return r.info
}
// validateRenderData validates the input data before rendering
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 for package: %s", data.PackageName)
}
// 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
}
// estimatedBufferSize calculates the estimated buffer size needed for rendering
func estimatedBufferSize(data RenderData) int {
// Base size for package structure
baseSize := 1024 // ~1KB for base package structure
// Add size based on routes
routesSize := len(data.Routes) * 256 // ~256 bytes per route
// Add size based on imports
importsSize := len(data.Imports) * 64 // ~64 bytes per import
// Add size based on controllers
controllersSize := len(data.Controllers) * 128 // ~128 bytes per controller
return baseSize + routesSize + importsSize + controllersSize
}
// renderTemplate is the legacy function for backward compatibility
// Use NewRouteRenderer().Render() for new code
func renderTemplate(data RenderData) ([]byte, error) {
renderer := NewRouteRenderer()
if renderer == nil {
return nil, NewRouteError(ErrTemplateFailed, "failed to create route renderer")
}
return renderer.Render(data)
}

View File

@@ -10,14 +10,15 @@ import (
"github.com/pkg/errors"
"github.com/samber/lo"
log "github.com/sirupsen/logrus"
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
)
type RouteDefinition struct {
Path string
Name string
Imports []string
Actions []ActionDefinition
FilePath string
Path string
Name string
Imports []string
Actions []ActionDefinition
}
type ActionDefinition struct {
@@ -72,14 +73,227 @@ func ParseFile(file string) []RouteDefinition {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
if err != nil {
log.Println("ERR: ", err)
log.WithError(err).Error("Failed to parse file")
return nil
}
imports := extractImports(node)
parser := &routeParser{
file: file,
node: node,
imports: imports,
routes: make(map[string]RouteDefinition),
actions: make(map[string][]ActionDefinition),
usedImports: make(map[string][]string),
}
return parser.parse()
}
type routeParser struct {
file string
node *ast.File
imports map[string]string
routes map[string]RouteDefinition
actions map[string][]ActionDefinition
usedImports map[string][]string
}
func (p *routeParser) parse() []RouteDefinition {
p.parseFunctionDeclarations()
return p.buildResult()
}
func (p *routeParser) parseFunctionDeclarations() {
for _, decl := range p.node.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !p.isValidFunctionDeclaration(funcDecl, ok) {
continue
}
recvType := p.extractReceiverType(funcDecl)
p.initializeRoute(recvType)
routeInfo := p.extractRouteInfo(funcDecl)
if routeInfo == nil {
continue
}
action := p.buildAction(funcDecl, routeInfo)
p.actions[recvType] = append(p.actions[recvType], action)
}
}
func (p *routeParser) isValidFunctionDeclaration(decl *ast.FuncDecl, ok bool) bool {
return ok &&
decl.Recv != nil &&
decl.Doc != nil
}
func (p *routeParser) extractReceiverType(decl *ast.FuncDecl) string {
return decl.Recv.List[0].Type.(*ast.StarExpr).X.(*ast.Ident).Name
}
func (p *routeParser) initializeRoute(recvType string) {
if _, exists := p.routes[recvType]; !exists {
p.routes[recvType] = RouteDefinition{
Name: recvType,
FilePath: p.file,
Actions: []ActionDefinition{},
}
p.actions[recvType] = []ActionDefinition{}
}
}
type routeInfo struct {
path string
method string
bindParams []ParamDefinition
}
func (p *routeParser) extractRouteInfo(decl *ast.FuncDecl) *routeInfo {
var info routeInfo
var err error
for _, comment := range decl.Doc.List {
line := normalizeCommentLine(comment.Text)
if strings.HasPrefix(line, "@Router") {
info.path, info.method, err = parseRouteComment(line)
if err != nil {
log.WithError(err).WithFields(log.Fields{
"file": p.file,
"action": decl.Name.Name,
}).Error("Invalid route definition")
return nil
}
}
if strings.HasPrefix(line, "@Bind") {
info.bindParams = append(info.bindParams, parseRouteBind(line))
}
}
if info.path == "" || info.method == "" {
return nil
}
log.WithFields(log.Fields{
"file": p.file,
"action": decl.Name.Name,
"path": info.path,
"method": info.method,
}).Info("Found route")
return &info
}
func (p *routeParser) buildAction(decl *ast.FuncDecl, routeInfo *routeInfo) ActionDefinition {
orderBindParams := p.processFunctionParameters(decl, routeInfo.bindParams)
hasData := false
if decl.Type != nil && decl.Type.Results != nil {
hasData = len(decl.Type.Results.List) > 1
}
return ActionDefinition{
Route: routeInfo.path,
Method: strings.ToUpper(routeInfo.method),
Name: decl.Name.Name,
HasData: hasData,
Params: orderBindParams,
}
}
func (p *routeParser) processFunctionParameters(decl *ast.FuncDecl, bindParams []ParamDefinition) []ParamDefinition {
var orderBindParams []ParamDefinition
if decl.Type == nil || decl.Type.Params == nil {
return orderBindParams
}
for _, param := range decl.Type.Params.List {
paramType := extractParameterType(param.Type)
if isContextParameter(paramType) {
continue
}
p.trackUsedImports(decl.Recv.List[0].Type.(*ast.StarExpr).X.(*ast.Ident).Name, paramType)
for _, paramName := range param.Names {
for i, bindParam := range bindParams {
if bindParam.Name == paramName.Name {
bindParams[i].Type = p.normalizeParameterType(paramType, bindParam.Position)
orderBindParams = append(orderBindParams, bindParams[i])
break
}
}
}
}
return orderBindParams
}
func (p *routeParser) trackUsedImports(recvType, paramType string) {
pkgParts := strings.Split(strings.Trim(paramType, "*"), ".")
if len(pkgParts) == 2 {
if importPath, exists := p.imports[pkgParts[0]]; exists {
p.usedImports[recvType] = append(p.usedImports[recvType], importPath)
}
}
}
func (p *routeParser) normalizeParameterType(paramType string, position Position) string {
if position != PositionLocal {
return strings.TrimPrefix(paramType, "*")
}
return paramType
}
func (p *routeParser) buildResult() []RouteDefinition {
var items []RouteDefinition
for k, route := range p.routes {
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)
}
}
return items
}
func (p *routeParser) getUniqueImports(recvType string) []string {
if imports, exists := p.usedImports[recvType]; exists {
return lo.Uniq(imports)
}
return []string{}
}
func extractImports(node *ast.File) map[string]string {
imports := make(map[string]string)
for _, imp := range node.Imports {
pkg := strings.Trim(imp.Path.Value, "\"")
name := gomod.GetPackageModuleName(pkg)
// Handle empty or invalid package paths
if pkg == "" {
continue
}
// Use the last part of the package path as the name
// This avoids calling gomod.GetPackageModuleName which can cause panics
name := pkg
if lastSlash := strings.LastIndex(pkg, "/"); lastSlash >= 0 {
name = pkg[lastSlash+1:]
}
if imp.Name != nil {
name = imp.Name.Name
pkg = fmt.Sprintf(`%s %q`, name, pkg)
@@ -88,140 +302,28 @@ func ParseFile(file string) []RouteDefinition {
}
imports[name] = fmt.Sprintf("%q", pkg)
}
return imports
}
routes := make(map[string]RouteDefinition)
actions := make(map[string][]ActionDefinition)
usedImports := make(map[string][]string)
func normalizeCommentLine(line string) string {
return strings.TrimSpace(strings.TrimLeft(line, "/ \t"))
}
// 再去遍历 struct 的方法去
for _, decl := range node.Decls {
decl, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
// 普通方法不要
if decl.Recv == nil {
continue
}
// 没有Doc不要
if decl.Doc == nil {
continue
}
recvType := decl.Recv.List[0].Type.(*ast.StarExpr).X.(*ast.Ident).Name
if _, ok := routes[recvType]; !ok {
routes[recvType] = RouteDefinition{
Name: recvType,
Path: file,
Actions: []ActionDefinition{},
}
actions[recvType] = []ActionDefinition{}
}
bindParams := []ParamDefinition{}
// Doc 中把 @Router 的定义拿出来, Route 格式为 /user/:id [get] 两部分,表示路径和请求方法
var path, method string
var err error
for _, l := range decl.Doc.List {
line := strings.TrimLeft(l.Text, "/ \t")
line = strings.TrimSpace(line)
// 路由需要一些切换
if strings.HasPrefix(line, "@Router") {
path, method, err = parseRouteComment(line)
if err != nil {
log.Fatal(errors.Wrapf(err, "file: %s, action: %s", file, decl.Name.Name))
}
}
if strings.HasPrefix(line, "@Bind") {
//@Bind name [uri|query|path|body|header|cookie] [key()] [table()] [model(<pkg>.<Type>[:<field>])]
bindParams = append(bindParams, parseRouteBind(line))
}
}
if path == "" || method == "" {
continue
}
log.WithField("file", file).
WithField("action", decl.Name.Name).
WithField("path", path).
WithField("method", method).
Info("get router")
// 拿参数列表去, 忽略 context.Context 参数
orderBindParams := []ParamDefinition{}
for _, param := range decl.Type.Params.List {
// paramsType, ok := param.Type.(*ast.SelectorExpr)
var typ string
switch param.Type.(type) {
case *ast.Ident:
typ = param.Type.(*ast.Ident).Name
case *ast.StarExpr:
paramsType := param.Type.(*ast.StarExpr)
switch paramsType.X.(type) {
case *ast.SelectorExpr:
X := paramsType.X.(*ast.SelectorExpr)
typ = fmt.Sprintf("*%s.%s", X.X.(*ast.Ident).Name, X.Sel.Name)
default:
typ = fmt.Sprintf("*%s", paramsType.X.(*ast.Ident).Name)
}
case *ast.SelectorExpr:
typ = fmt.Sprintf("%s.%s", param.Type.(*ast.SelectorExpr).X.(*ast.Ident).Name, param.Type.(*ast.SelectorExpr).Sel.Name)
}
if strings.HasSuffix(typ, "Context") || strings.HasSuffix(typ, "Ctx") {
continue
}
pkgName := strings.Split(strings.Trim(typ, "*"), ".")
if len(pkgName) == 2 {
usedImports[recvType] = append(usedImports[recvType], imports[pkgName[0]])
}
for _, name := range param.Names {
for i, bindParam := range bindParams {
if bindParam.Name == name.Name {
if bindParams[i].Position != PositionLocal {
typ = strings.TrimPrefix(typ, "*")
}
bindParams[i].Type = typ
orderBindParams = append(orderBindParams, bindParams[i])
break
}
}
}
}
actions[recvType] = append(actions[recvType], ActionDefinition{
Route: path,
Method: strings.ToUpper(method),
Name: decl.Name.Name,
HasData: len(decl.Type.Results.List) > 1,
Params: orderBindParams,
})
func extractParameterType(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + extractParameterType(t.X)
case *ast.SelectorExpr:
return fmt.Sprintf("%s.%s", extractParameterType(t.X), t.Sel.Name)
default:
return ""
}
}
var items []RouteDefinition
for k, item := range routes {
a, ok := actions[k]
if !ok {
continue
}
item.Actions = a
item.Imports = []string{}
if im, ok := usedImports[k]; ok {
item.Imports = lo.Uniq(im)
}
items = append(items, item)
}
return items
func isContextParameter(paramType string) bool {
return strings.HasSuffix(paramType, "Context") || strings.HasSuffix(paramType, "Ctx")
}
func parseRouteComment(line string) (string, string, error) {
@@ -251,20 +353,26 @@ func parseRouteBind(bind string) ParamDefinition {
for i, part := range parts {
switch part {
case "@Bind":
param.Name = parts[i+1]
param.Position = positionFromString(parts[i+2])
case "key":
param.Key = parts[i+1]
case "model":
// Supported formats:
// - model(field:field_type) -> only specify model field/column;
mv := parts[i+1]
// if mv contains no dot, treat as field name directly
if mv == "" {
param.Model = "id"
break
if i+2 < len(parts) {
param.Name = parts[i+1]
param.Position = positionFromString(parts[i+2])
}
case "key":
if i+1 < len(parts) {
param.Key = parts[i+1]
}
case "model":
if i+1 < len(parts) {
// Supported formats:
// - model(field:field_type) -> only specify model field/column;
mv := parts[i+1]
// if mv contains no dot, treat as field name directly
if mv == "" {
param.Model = "id"
break
}
param.Model = mv
}
param.Model = mv
}
}
return param

541
pkg/ast/route/route_test.go Normal file
View File

@@ -0,0 +1,541 @@
package route
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRouteParsing(t *testing.T) {
t.Run("BasicRouteParsing", func(t *testing.T) {
// This test will fail until we improve the existing parsing
// GIVEN a Go file with basic route annotation
code := `
package main
// UserController defines user-related routes
type UserController struct {}
// @Router /users [get]
func (c *UserController) GetUser() error {
return nil
}
`
// Create a temporary file for testing
tmpFile := "/tmp/test_route.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing the file using existing API
routes := ParseFile(tmpFile)
// THEN it should return the route definition
// Note: Current implementation may not extract all info correctly
assert.NotEmpty(t, routes, "Should find at least one route")
route := routes[0]
assert.Equal(t, "/users", route.Path, "Should extract correct path")
// Find the GET action
getAction := findActionByMethod(route.Actions, "GET")
assert.NotNil(t, getAction, "Should find GET action")
})
}
func TestParameterBinding(t *testing.T) {
t.Run("ParameterBinding", func(t *testing.T) {
// GIVEN a Go file with parameter bindings
code := `
package main
type UserController struct {}
// @Router /users/:id [get]
// @Bind id (path) model()
// @Bind limit (query) model(limit:int)
func (c *UserController) GetUser(id string, limit int) {
}
`
// Create a temporary file for testing
tmpFile := "/tmp/test_params.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing the file using existing API
routes := ParseFile(tmpFile)
// THEN it should succeed and extract parameters
assert.NotEmpty(t, routes, "Should find at least one route")
route := routes[0]
getAction := findActionByMethod(route.Actions, "GET")
assert.NotNil(t, getAction, "Should find GET action")
assert.NotEmpty(t, getAction.Params, "GET action should have parameters")
// Verify path parameter
pathParam := findParameterByPosition(getAction.Params, PositionPath)
assert.NotNil(t, pathParam, "Should find path parameter")
assert.Equal(t, "id", pathParam.Name, "Path parameter should be named 'id'")
// Verify query parameter
queryParam := findParameterByPosition(getAction.Params, PositionQuery)
assert.NotNil(t, queryParam, "Should find query parameter")
assert.Equal(t, "limit", queryParam.Name, "Query parameter should be named 'limit'")
})
}
func TestErrorHandling(t *testing.T) {
t.Run("InvalidSyntax", func(t *testing.T) {
// GIVEN invalid Go code
code := `
package main
type UserController struct {}
// @Router /users [get] // Missing closing bracket
func (c *UserController) GetUser() {
`
// Create a temporary file for testing
tmpFile := "/tmp/test_invalid.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing the file using existing API
routes := ParseFile(tmpFile)
// THEN it should handle the error gracefully
// Note: Current implementation may log errors but return nil/empty
assert.Empty(t, routes, "Should return no routes on invalid syntax")
})
t.Run("EmptyRoute", func(t *testing.T) {
// GIVEN a route with empty path
code := `
package main
type UserController struct {}
// @Router [get]
func (c *UserController) GetUser() {
}
`
// Create a temporary file for testing
tmpFile := "/tmp/test_empty.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing the file using existing API
routes := ParseFile(tmpFile)
// THEN it should handle the validation appropriately
// Current implementation creates route entries but skips invalid route definitions
// So we expect 1 route but with no actions
assert.Equal(t, 1, len(routes), "Should create route entry for controller")
assert.Equal(t, 0, len(routes[0].Actions), "Should have no actions for invalid route")
})
}
// Helper functions (using existing data structures)
func findParameterByPosition(params []ParamDefinition, position Position) *ParamDefinition {
for _, param := range params {
if param.Position == position {
return &param
}
}
return nil
}
func findActionByMethod(actions []ActionDefinition, method string) *ActionDefinition {
for _, action := range actions {
if action.Method == method {
return &action
}
}
return nil
}
func TestBackwardCompatibility(t *testing.T) {
t.Run("ExistingAnnotationFormats", func(t *testing.T) {
// GIVEN existing annotation formats that must continue to work
code := `
package main
type UserController struct {
}
// @Router /api/v1/users [get]
// @Bind id (path) model()
// @Bind name (query) model(name:string)
func (c *UserController) GetUser(id int, name string) error {
return nil
}
// @Router /api/v1/users [post]
// @Bind user (body) model(*User)
func (c *UserController) CreateUser(user *User) error {
return nil
}
type HealthController struct {
}
// @Router /health [get]
func (c *HealthController) Check() error {
return nil
}
`
// Create a temporary file for testing
tmpFile := "/tmp/test_compatibility.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing the file using existing API
routes := ParseFile(tmpFile)
// THEN it should extract all routes without breaking
assert.NotEmpty(t, routes, "Should find routes")
assert.Equal(t, 2, len(routes), "Should find exactly 2 routes")
// Verify user route with multiple actions
userRoute := findRouteByPath(routes, "/api/v1/users")
assert.NotNil(t, userRoute, "Should find user route")
assert.Equal(t, 2, len(userRoute.Actions), "Should have GET and POST actions")
// Verify HTTP methods
getAction := findActionByMethod(userRoute.Actions, "GET")
assert.NotNil(t, getAction, "Should find GET action")
postAction := findActionByMethod(userRoute.Actions, "POST")
assert.NotNil(t, postAction, "Should find POST action")
// Verify parameter binding compatibility
assert.GreaterOrEqual(t, len(getAction.Params), 2, "GET action should have parameters bound")
// Verify health route
healthRoute := findRouteByPath(routes, "/health")
assert.NotNil(t, healthRoute, "Should find health route")
assert.Equal(t, 1, len(healthRoute.Actions), "Should have 1 action")
})
t.Run("SpecialCharactersInPaths", func(t *testing.T) {
// GIVEN routes with special characters that must work
code := `
package main
type ApiController struct {
}
// @Router /api/v1/users/:id/profile [get]
func (c *ApiController) GetUserProfile(id string) error {
return nil
}
// @Router /api/v1/orders/:order_id/items/:item_id [post]
func (c *ApiController) GetOrderItem(orderID, itemID string) error {
return nil
}
// @Router /download/*filename [put]
func (c *ApiController) DownloadFile(filename string) error {
return nil
}
`
tmpFile := "/tmp/test_special_paths.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing
routes := ParseFile(tmpFile)
// THEN special paths should be preserved
assert.Equal(t, 1, len(routes), "Should find 1 controller with multiple actions")
// All routes belong to ApiController, check that all actions are present
apiRoute := findRouteByPath(routes, "/api/v1/users/:id/profile")
assert.NotNil(t, apiRoute, "Should find API controller route")
assert.Equal(t, 3, len(apiRoute.Actions), "Should have 3 actions")
// Verify all three methods are present
actionMethods := make(map[string]bool)
for _, action := range apiRoute.Actions {
actionMethods[action.Method] = true
}
assert.True(t, actionMethods["GET"], "Should have GET method")
assert.True(t, actionMethods["POST"], "Should have POST method")
assert.True(t, actionMethods["PUT"], "Should have PUT method")
assert.Equal(t, 3, len(actionMethods), "Should have 3 different methods")
// Verify we can find actions by checking names
var foundUserProfile, foundOrderItem, foundDownload bool
for _, action := range apiRoute.Actions {
switch action.Name {
case "GetUserProfile":
foundUserProfile = true
case "GetOrderItem":
foundOrderItem = true
case "DownloadFile":
foundDownload = true
}
}
assert.True(t, foundUserProfile, "Should find GetUserProfile action")
assert.True(t, foundOrderItem, "Should find GetOrderItem action")
assert.True(t, foundDownload, "Should find DownloadFile action")
})
}
// Helper function to find route by path
func findRouteByPath(routes []RouteDefinition, path string) *RouteDefinition {
for _, route := range routes {
if route.Path == path {
return &route
}
}
return nil
}
func TestCLIIntegration(t *testing.T) {
t.Run("ParseFileIntegration", func(t *testing.T) {
// GIVEN a realistic controller file structure
code := `
package main
import (
"net/http"
)
type UserController struct {
}
// @Router /api/v1/users [get]
// @Bind id (path) model()
// @Bind filter (query) model(filter:UserFilter)
func (c *UserController) GetUser(id int, filter string) error {
return nil
}
// @Router /api/v1/users [post]
// @Bind user (body) model(*User)
func (c *UserController) CreateUser(user *User) error {
return nil
}
type ProductController struct {
}
// @Router /api/v1/products [get]
// @Bind category (query) model(category:string)
func (c *ProductController) GetProducts(category string) error {
return nil
}
type HealthController struct {
}
// @Router /health [get]
func (c *HealthController) Check() error {
return nil
}
`
// Create a realistic file structure
tmpDir := "/tmp/test_app"
httpDir := tmpDir + "/app/http"
err := os.MkdirAll(httpDir, 0755)
assert.NoError(t, err, "Should create directory structure")
defer os.RemoveAll(tmpDir)
// Write controller file
controllerFile := httpDir + "/user_controller.go"
err = os.WriteFile(controllerFile, []byte(code), 0644)
assert.NoError(t, err, "Should write controller file")
// WHEN parsing using the same method as CLI
routes := ParseFile(controllerFile)
// THEN it should work as expected by the CLI
assert.NotEmpty(t, routes, "Should parse routes successfully")
assert.Equal(t, 3, len(routes), "Should find 3 controllers")
// Verify the data structure matches CLI expectations
userRoute := findRouteByPath(routes, "/api/v1/users")
assert.NotNil(t, userRoute, "Should find user route")
assert.Equal(t, "UserController", userRoute.Name, "Should preserve controller name")
assert.Equal(t, 2, len(userRoute.Actions), "Should have GET and POST actions")
// Verify parameter structure is compatible
getAction := findActionByMethod(userRoute.Actions, "GET")
assert.NotNil(t, getAction, "Should have GET action")
// Verify the data can be processed by the existing grouping logic
// (similar to lo.GroupBy in cmd/gen_route.go:105)
routeGroups := make(map[string][]RouteDefinition)
for _, route := range routes {
dirPath := filepath.Dir(route.Path)
routeGroups[dirPath] = append(routeGroups[dirPath], route)
}
assert.Greater(t, len(routeGroups), 0, "Should be groupable by path")
})
t.Run("EmptyFileHandling", func(t *testing.T) {
// GIVEN an empty Go file
code := `
package main
type EmptyController struct {
}
`
tmpFile := "/tmp/test_empty.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing
routes := ParseFile(tmpFile)
// THEN it should handle gracefully (no panic, empty result)
assert.Empty(t, routes, "Should return empty routes for file without annotations")
})
t.Run("FileWithSyntaxErrors", func(t *testing.T) {
// GIVEN a file with syntax errors
code := `
package main
// @Router /test [get]
// This is not valid Go syntax
type BrokenController struct {
`
tmpFile := "/tmp/test_broken.go"
err := os.WriteFile(tmpFile, []byte(code), 0644)
assert.NoError(t, err, "Should create temp file")
defer os.Remove(tmpFile)
// WHEN parsing
routes := ParseFile(tmpFile)
// THEN it should handle gracefully
// The current implementation may return empty or partial results
// This test verifies the CLI won't crash
assert.True(t, len(routes) == 0, "Should handle syntax errors gracefully")
})
}
func TestRouteRenderer(t *testing.T) {
t.Run("NewRouteRenderer", func(t *testing.T) {
renderer := NewRouteRenderer()
assert.NotNil(t, renderer)
assert.NotNil(t, renderer.logger)
info := renderer.GetTemplateInfo()
assert.Equal(t, "router", info.Name)
assert.Equal(t, "1.0.0", info.Version)
assert.Contains(t, info.Functions, "sprig")
})
t.Run("RendererValidation", func(t *testing.T) {
renderer := NewRouteRenderer()
assert.NotNil(t, renderer)
err := renderer.Validate()
assert.NoError(t, err)
})
t.Run("RenderValidData", func(t *testing.T) {
renderer := NewRouteRenderer()
assert.NotNil(t, renderer)
data := RenderData{
PackageName: "main",
ProjectPackage: "test/project",
Imports: []string{`"fmt"`},
Controllers: []string{"userController *UserController"},
Routes: map[string][]Router{
"UserController": {
{
Method: "Get",
Route: "/users",
Controller: "userController",
Action: "GetUser",
Func: "Func0",
},
},
},
RouteGroups: []string{"UserController"},
}
content, err := renderer.Render(data)
assert.NoError(t, err)
assert.NotEmpty(t, content)
assert.Contains(t, string(content), "package main")
assert.Contains(t, string(content), "func (r *Routes) Register")
})
t.Run("RenderInvalidData", func(t *testing.T) {
renderer := NewRouteRenderer()
assert.NotNil(t, renderer)
data := RenderData{
PackageName: "", // Invalid empty package name
Routes: map[string][]Router{},
}
content, err := renderer.Render(data)
assert.Error(t, err)
assert.Nil(t, content)
assert.Contains(t, err.Error(), "package name cannot be empty")
})
t.Run("RenderNoRoutes", func(t *testing.T) {
renderer := NewRouteRenderer()
assert.NotNil(t, renderer)
data := RenderData{
PackageName: "main",
Routes: map[string][]Router{}, // Empty routes
}
content, err := renderer.Render(data)
assert.Error(t, err)
assert.Nil(t, content)
assert.Contains(t, err.Error(), "no routes to render")
})
t.Run("LegacyRenderTemplate", func(t *testing.T) {
data := RenderData{
PackageName: "main",
ProjectPackage: "test/project",
Imports: []string{`"fmt"`},
Controllers: []string{"userController *UserController"},
Routes: map[string][]Router{
"UserController": {
{
Method: "Get",
Route: "/users",
Controller: "userController",
Action: "GetUser",
Func: "Func0",
},
},
},
RouteGroups: []string{"UserController"},
}
content, err := renderTemplate(data)
assert.NoError(t, err)
assert.NotEmpty(t, content)
assert.Contains(t, string(content), "package main")
})
}

View File

@@ -1,10 +1,14 @@
// Code generated by the atomctl ; DO NOT EDIT.
// Code generated by atomctl. DO NOT EDIT.
// Package {{.PackageName}} provides HTTP route definitions and registration
// for the {{.ProjectPackage}} application.
package {{.PackageName}}
import (
{{- if .Imports }}
{{- range .Imports }}
{{.}}
{{- end }}
{{- end }}
. "go.ipao.vip/atom/fen"
_ "go.ipao.vip/atom"
@@ -13,34 +17,58 @@ import (
log "github.com/sirupsen/logrus"
)
// Routes implements the HttpRoute contract and provides route registration
// for all controllers in the {{.PackageName}} module.
//
// @provider contracts.HttpRoute atom.GroupRoutes
type Routes struct {
log *log.Entry `inject:"false"`
{{- if .Controllers }}
// Controller instances
{{- range .Controllers }}
{{.}}
{{- end }}
{{- end }}
}
// Prepare initializes the routes provider with logging configuration.
func (r *Routes) Prepare() error {
r.log = log.WithField("module", "routes.{{.PackageName}}")
r.log.Info("Initializing routes module")
return nil
}
// Name returns the unique identifier for this routes provider.
func (r *Routes) Name() string {
return "{{.PackageName}}"
}
// Register registers all HTTP routes with the provided fiber router.
// Each route is registered with its corresponding controller action and parameter bindings.
func (r *Routes) Register(router fiber.Router) {
{{- if .RouteGroups }}
{{- range $key := .RouteGroups }}
// : {{$key}}
// Register routes for controller: {{$key}}
{{- $value := index $.Routes $key }}
{{- if $value }}
{{- range $value }}
{{- if .Route }}
r.log.Debugf("Registering route: {{.Method}} {{.Route}} -> {{.Controller}}.{{.Action}}")
router.{{.Method}}("{{.Route}}", {{.Func}}(
r.{{.Controller}}.{{.Action}},
{{- range .Params}}
{{- if .Params }}
{{- range .Params }}
{{.}},
{{- end }}
{{- end }}
))
{{ end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- else }}
r.log.Warn("No routes found to register")
{{- end }}
r.log.Info("Successfully registered all routes")
}