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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user