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

@@ -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
}