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:
24
CLAUDE.md
Normal file
24
CLAUDE.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# atomctl Development Guidelines
|
||||
|
||||
Auto-generated from all feature plans. Last updated: 2025-09-22
|
||||
|
||||
## Active Technologies
|
||||
- + (002-refactor-ast-gen)
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
src/
|
||||
tests/
|
||||
```
|
||||
|
||||
## Commands
|
||||
# Add commands for
|
||||
|
||||
## Code Style
|
||||
: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 002-refactor-ast-gen: Added +
|
||||
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
@@ -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.Path)
|
||||
return filepath.Dir(item.FilePath)
|
||||
})
|
||||
|
||||
for path, routes := range routeGroups {
|
||||
|
||||
@@ -16,70 +16,128 @@ type RenderBuildOpts struct {
|
||||
}
|
||||
|
||||
func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
|
||||
rd := RenderData{
|
||||
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
|
||||
}
|
||||
|
||||
func (b *renderDataBuilder) build() (RenderData, error) {
|
||||
b.processRoutes()
|
||||
b.addRequiredImports()
|
||||
b.dedupeAndSortImports()
|
||||
b.dedupeAndSortControllers()
|
||||
b.sortRouteGroups()
|
||||
|
||||
return b.data, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
funcName := fmt.Sprintf("Func%d", len(action.Params))
|
||||
if action.HasData {
|
||||
funcName = "Data" + funcName
|
||||
router := b.buildRouter(route, action)
|
||||
b.data.Routes[route.Name] = append(b.data.Routes[route.Name], router)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
func (b *renderDataBuilder) buildRouter(route RouteDefinition, action ActionDefinition) Router {
|
||||
funcName := b.generateFunctionName(action)
|
||||
params := b.buildParameters(action.Params)
|
||||
|
||||
rd.Routes[route.Name] = append(rd.Routes[route.Name], Router{
|
||||
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)
|
||||
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(rd.RouteGroups)
|
||||
for _, k := range rd.RouteGroups {
|
||||
items := rd.Routes[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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -89,58 +147,106 @@ func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
|
||||
}
|
||||
return items[i].Action < items[j].Action
|
||||
})
|
||||
rd.Routes[k] = items
|
||||
}
|
||||
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
func buildParamToken(item ParamDefinition) string {
|
||||
key := item.Name
|
||||
if item.Key != "" {
|
||||
key = item.Key
|
||||
}
|
||||
key := item.getKey()
|
||||
builder := ¶mTokenBuilder{item: item, key: key}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
switch item.Position {
|
||||
func (item ParamDefinition) getKey() string {
|
||||
if item.Key != "" {
|
||||
return item.Key
|
||||
}
|
||||
return item.Name
|
||||
}
|
||||
|
||||
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]
|
||||
return b.buildPathParam()
|
||||
case PositionLocal:
|
||||
return b.buildLocalParam()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
field = item.Model
|
||||
}
|
||||
|
||||
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, item.Type, fieldType, key, item.Type, field)
|
||||
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]
|
||||
}
|
||||
return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
|
||||
case PositionLocal:
|
||||
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
|
||||
} else {
|
||||
field = b.item.Model
|
||||
}
|
||||
return ""
|
||||
|
||||
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
58
pkg/ast/route/errors.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,18 +5,187 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var routerTmpl = template.Must(template.New("route").
|
||||
// TemplateRenderer defines the interface for template rendering operations
|
||||
type TemplateRenderer interface {
|
||||
Render(data RenderData) ([]byte, error)
|
||||
Validate() error
|
||||
GetTemplateInfo() TemplateInfo
|
||||
}
|
||||
|
||||
// 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(routeTpl),
|
||||
)
|
||||
Option("missingkey=error")
|
||||
|
||||
func renderTemplate(data RenderData) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := routerTmpl.Execute(&buf, data); err != nil {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ 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 {
|
||||
FilePath string
|
||||
Path string
|
||||
Name string
|
||||
Imports []string
|
||||
@@ -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) {
|
||||
func extractParameterType(expr ast.Expr) string {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
typ = param.Type.(*ast.Ident).Name
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
paramsType := param.Type.(*ast.StarExpr)
|
||||
switch paramsType.X.(type) {
|
||||
return "*" + extractParameterType(t.X)
|
||||
case *ast.SelectorExpr:
|
||||
X := paramsType.X.(*ast.SelectorExpr)
|
||||
typ = fmt.Sprintf("*%s.%s", X.X.(*ast.Ident).Name, X.Sel.Name)
|
||||
return fmt.Sprintf("%s.%s", extractParameterType(t.X), t.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)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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,11 +353,16 @@ func parseRouteBind(bind string) ParamDefinition {
|
||||
for i, part := range parts {
|
||||
switch part {
|
||||
case "@Bind":
|
||||
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]
|
||||
@@ -267,5 +374,6 @@ func parseRouteBind(bind string) ParamDefinition {
|
||||
param.Model = mv
|
||||
}
|
||||
}
|
||||
}
|
||||
return param
|
||||
}
|
||||
|
||||
541
pkg/ast/route/route_test.go
Normal file
541
pkg/ast/route/route_test.go
Normal 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 ¶m
|
||||
}
|
||||
}
|
||||
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")
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
239
specs/002-refactor-ast-gen/contracts/route_builder_test.go
Normal file
239
specs/002-refactor-ast-gen/contracts/route_builder_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// RouteBuilderContract defines the contract tests for RouteBuilder implementations
|
||||
type RouteBuilderContract struct {
|
||||
builder RouteBuilder
|
||||
}
|
||||
|
||||
// RouteBuilder interface definition for contract testing
|
||||
type RouteBuilder interface {
|
||||
BuildFromTypeSpec(typeSpec *ast.TypeSpec, decl *ast.GenDecl, context *BuilderContext) (RouteDefinition, error)
|
||||
BuildFromComment(comment string, context *BuilderContext) (RouteDefinition, error)
|
||||
ValidateDefinition(def *RouteDefinition) error
|
||||
}
|
||||
|
||||
// BuilderContext represents builder context (simplified for testing)
|
||||
type BuilderContext struct {
|
||||
FilePath string
|
||||
PackageName string
|
||||
ImportContext *ImportContext
|
||||
ASTFile *ast.File
|
||||
}
|
||||
|
||||
// ImportContext represents import context (simplified for testing)
|
||||
type ImportContext struct {
|
||||
Imports map[string]string
|
||||
}
|
||||
|
||||
// NewRouteBuilderContract creates a new contract test instance
|
||||
func NewRouteBuilderContract(builder RouteBuilder) *RouteBuilderContract {
|
||||
return &RouteBuilderContract{builder: builder}
|
||||
}
|
||||
|
||||
// TestSuite runs all contract tests for RouteBuilder
|
||||
func (c *RouteBuilderContract) TestSuite(t *testing.T) {
|
||||
t.Run("RouteBuilder_BuildFromTypeSpec_BasicRoute", c.testBuildFromTypeSpecBasicRoute)
|
||||
t.Run("RouteBuilder_BuildFromTypeSpec_WithParameters", c.testBuildFromTypeSpecWithParameters)
|
||||
t.Run("RouteBuilder_BuildFromTypeSpec_InvalidInput", c.testBuildFromTypeSpecInvalidInput)
|
||||
t.Run("RouteBuilder_BuildFromComment_SimpleComment", c.testBuildFromCommentSimpleComment)
|
||||
t.Run("RouteBuilder_BuildFromComment_ComplexComment", c.testBuildFromCommentComplexComment)
|
||||
t.Run("RouteBuilder_ValidateDefinition_ValidRoute", c.testValidateDefinitionValidRoute)
|
||||
t.Run("RouteBuilder_ValidateDefinition_InvalidRoute", c.testValidateDefinitionInvalidRoute)
|
||||
}
|
||||
|
||||
// Contract Tests
|
||||
|
||||
func (c *RouteBuilderContract) testBuildFromTypeSpecBasicRoute(t *testing.T) {
|
||||
// GIVEN a valid type specification and declaration
|
||||
typeSpec := &ast.TypeSpec{
|
||||
Name: &ast.Ident{Name: "UserController"},
|
||||
}
|
||||
|
||||
decl := &ast.GenDecl{
|
||||
Doc: &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: "// @Router /users [get]"},
|
||||
},
|
||||
},
|
||||
Specs: []ast.Spec{typeSpec},
|
||||
}
|
||||
|
||||
context := &BuilderContext{
|
||||
FilePath: "UserController.go",
|
||||
PackageName: "controllers",
|
||||
ImportContext: &ImportContext{
|
||||
Imports: make(map[string]string),
|
||||
},
|
||||
}
|
||||
|
||||
// WHEN building route from type spec
|
||||
route, err := c.builder.BuildFromTypeSpec(typeSpec, decl, context)
|
||||
|
||||
// THEN it should succeed and return valid route definition
|
||||
assert.NoError(t, err, "BuildFromTypeSpec should not error")
|
||||
assert.NotNil(t, route, "Should return route definition")
|
||||
|
||||
assert.Equal(t, "UserController", route.StructName, "Should extract correct struct name")
|
||||
assert.Equal(t, "/users", route.Path, "Should extract correct path")
|
||||
assert.Contains(t, route.Methods, "GET", "Should include GET method")
|
||||
}
|
||||
|
||||
func (c *RouteBuilderContract) testBuildFromTypeSpecWithParameters(t *testing.T) {
|
||||
// GIVEN a type specification with parameter bindings
|
||||
typeSpec := &ast.TypeSpec{
|
||||
Name: &ast.Ident{Name: "UserController"},
|
||||
}
|
||||
|
||||
decl := &ast.GenDecl{
|
||||
Doc: &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: "// @Router /users/:id [get]"},
|
||||
{Text: "// @Bind id (path) model()"},
|
||||
{Text: "// @Bind limit (query) model(limit:int)"},
|
||||
},
|
||||
},
|
||||
Specs: []ast.Spec{typeSpec},
|
||||
}
|
||||
|
||||
context := &BuilderContext{
|
||||
FilePath: "UserController.go",
|
||||
PackageName: "controllers",
|
||||
ImportContext: &ImportContext{
|
||||
Imports: map[string]string{
|
||||
"model": "go.ipao.vip/gen/model",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// WHEN building route from type spec
|
||||
route, err := c.builder.BuildFromTypeSpec(typeSpec, decl, context)
|
||||
|
||||
// THEN it should succeed and extract parameters
|
||||
assert.NoError(t, err, "BuildFromTypeSpec should not error")
|
||||
assert.NotNil(t, route, "Should return route definition")
|
||||
|
||||
assert.NotEmpty(t, route.Parameters, "Route should have parameters")
|
||||
|
||||
// Verify path parameter
|
||||
pathParam := findParameterByPosition(route.Parameters, "path")
|
||||
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(route.Parameters, "query")
|
||||
assert.NotNil(t, queryParam, "Should find query parameter")
|
||||
assert.Equal(t, "limit", queryParam.Name, "Query parameter should be named 'limit'")
|
||||
}
|
||||
|
||||
func (c *RouteBuilderContract) testBuildFromTypeSpecInvalidInput(t *testing.T) {
|
||||
// GIVEN an invalid type specification (nil)
|
||||
var typeSpec *ast.TypeSpec = nil
|
||||
decl := &ast.GenDecl{}
|
||||
context := &BuilderContext{}
|
||||
|
||||
// WHEN building route from invalid type spec
|
||||
route, err := c.builder.BuildFromTypeSpec(typeSpec, decl, context)
|
||||
|
||||
// THEN it should fail with appropriate error
|
||||
assert.Error(t, err, "BuildFromTypeSpec should error on invalid input")
|
||||
assert.Equal(t, RouteDefinition{}, route, "Should return empty route on error")
|
||||
}
|
||||
|
||||
func (c *RouteBuilderContract) testBuildFromCommentSimpleComment(t *testing.T) {
|
||||
// GIVEN a simple route comment
|
||||
comment := `@Router /users [get]`
|
||||
context := &BuilderContext{
|
||||
FilePath: "UserController.go",
|
||||
PackageName: "controllers",
|
||||
}
|
||||
|
||||
// WHEN building route from comment
|
||||
route, err := c.builder.BuildFromComment(comment, context)
|
||||
|
||||
// THEN it should succeed and return valid route
|
||||
assert.NoError(t, err, "BuildFromComment should not error")
|
||||
assert.NotNil(t, route, "Should return route definition")
|
||||
|
||||
assert.Equal(t, "/users", route.Path, "Should extract correct path")
|
||||
assert.Contains(t, route.Methods, "GET", "Should include GET method")
|
||||
}
|
||||
|
||||
func (c *RouteBuilderContract) testBuildFromCommentComplexComment(t *testing.T) {
|
||||
// GIVEN a complex route comment with parameters
|
||||
comment := `@Router /users/:id [get,put]
|
||||
@Bind id (path) model()
|
||||
@Bind user (body) model(User)`
|
||||
context := &BuilderContext{
|
||||
FilePath: "UserController.go",
|
||||
PackageName: "controllers",
|
||||
ImportContext: &ImportContext{
|
||||
Imports: map[string]string{
|
||||
"model": "go.ipao.vip/gen/model",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// WHEN building route from comment
|
||||
route, err := c.builder.BuildFromComment(comment, context)
|
||||
|
||||
// THEN it should succeed and extract all information
|
||||
assert.NoError(t, err, "BuildFromComment should not error")
|
||||
assert.NotNil(t, route, "Should return route definition")
|
||||
|
||||
assert.Equal(t, "/users/:id", route.Path, "Should extract correct path")
|
||||
assert.Contains(t, route.Methods, "GET", "Should include GET method")
|
||||
assert.Contains(t, route.Methods, "PUT", "Should include PUT method")
|
||||
|
||||
assert.NotEmpty(t, route.Parameters, "Route should have parameters")
|
||||
}
|
||||
|
||||
func (c *RouteBuilderContract) testValidateDefinitionValidRoute(t *testing.T) {
|
||||
// GIVEN a valid route definition
|
||||
route := RouteDefinition{
|
||||
StructName: "UserController",
|
||||
Path: "/users",
|
||||
Methods: []string{"GET", "POST"},
|
||||
Parameters: []ParamDefinition{
|
||||
{Name: "id", Position: "path", Type: "int"},
|
||||
},
|
||||
}
|
||||
|
||||
// WHEN validating the route definition
|
||||
err := c.builder.ValidateDefinition(&route)
|
||||
|
||||
// THEN it should succeed
|
||||
assert.NoError(t, err, "ValidateDefinition should not error on valid route")
|
||||
}
|
||||
|
||||
func (c *RouteBuilderContract) testValidateDefinitionInvalidRoute(t *testing.T) {
|
||||
// GIVEN an invalid route definition (empty path)
|
||||
route := RouteDefinition{
|
||||
StructName: "UserController",
|
||||
Path: "", // Empty path is invalid
|
||||
Methods: []string{"GET"},
|
||||
}
|
||||
|
||||
// WHEN validating the route definition
|
||||
err := c.builder.ValidateDefinition(&route)
|
||||
|
||||
// THEN it should fail
|
||||
assert.Error(t, err, "ValidateDefinition should error on invalid route")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func findParameterByPosition(params []ParamDefinition, position string) *ParamDefinition {
|
||||
for _, param := range params {
|
||||
if param.Position == position {
|
||||
return ¶m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
261
specs/002-refactor-ast-gen/contracts/route_parser_test.go
Normal file
261
specs/002-refactor-ast-gen/contracts/route_parser_test.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// RouteParserContract defines the contract tests for RouteParser implementations
|
||||
type RouteParserContract struct {
|
||||
parser RouteParser
|
||||
}
|
||||
|
||||
// RouteParser interface definition for contract testing
|
||||
type RouteParser interface {
|
||||
ParseFile(filePath string) ([]RouteDefinition, error)
|
||||
ParseDir(dirPath string) ([]RouteDefinition, error)
|
||||
ParseString(code string) ([]RouteDefinition, error)
|
||||
SetConfig(config *RouteParserConfig) error
|
||||
GetConfig() *RouteParserConfig
|
||||
GetContext() *ParserContext
|
||||
GetDiagnostics() []Diagnostic
|
||||
}
|
||||
|
||||
// RouteDefinition represents a route definition (simplified for testing)
|
||||
type RouteDefinition struct {
|
||||
StructName string
|
||||
Path string
|
||||
Methods []string
|
||||
Parameters []ParamDefinition
|
||||
Imports map[string]string
|
||||
}
|
||||
|
||||
// ParamDefinition represents a parameter definition (simplified for testing)
|
||||
type ParamDefinition struct {
|
||||
Name string
|
||||
Position string
|
||||
Type string
|
||||
}
|
||||
|
||||
// RouteParserConfig represents parser configuration (simplified for testing)
|
||||
type RouteParserConfig struct {
|
||||
StrictMode bool
|
||||
ParseComments bool
|
||||
SourceLocations bool
|
||||
}
|
||||
|
||||
// ParserContext represents parser context (simplified for testing)
|
||||
type ParserContext struct {
|
||||
WorkingDir string
|
||||
ModuleName string
|
||||
}
|
||||
|
||||
// Diagnostic represents diagnostic information (simplified for testing)
|
||||
type Diagnostic struct {
|
||||
Level string
|
||||
Message string
|
||||
File string
|
||||
}
|
||||
|
||||
// NewRouteParserContract creates a new contract test instance
|
||||
func NewRouteParserContract(parser RouteParser) *RouteParserContract {
|
||||
return &RouteParserContract{parser: parser}
|
||||
}
|
||||
|
||||
// TestSuite runs all contract tests for RouteParser
|
||||
func (c *RouteParserContract) TestSuite(t *testing.T) {
|
||||
t.Run("RouteParser_ParseFile_BasicRoute", c.testParseFileBasicRoute)
|
||||
t.Run("RouteParser_ParseFile_WithParameters", c.testParseFileWithParameters)
|
||||
t.Run("RouteParser_ParseFile_InvalidSyntax", c.testParseFileInvalidSyntax)
|
||||
t.Run("RouteParser_ParseFile_NonexistentFile", c.testParseFileNonexistentFile)
|
||||
t.Run("RouteParser_ParseString_SimpleRoute", c.testParseStringSimpleRoute)
|
||||
t.Run("RouteParser_ParseString_MultipleRoutes", c.testParseStringMultipleRoutes)
|
||||
t.Run("RouteParser_ParseDir_EmptyDirectory", c.testParseDirEmptyDirectory)
|
||||
t.Run("RouteParser_Configuration", c.testConfiguration)
|
||||
t.Run("RouteParser_Diagnostics", c.testDiagnostics)
|
||||
}
|
||||
|
||||
// Contract Tests
|
||||
|
||||
func (c *RouteParserContract) testParseFileBasicRoute(t *testing.T) {
|
||||
// GIVEN a Go file with basic route annotation
|
||||
filePath := "testdata/basic_route.go"
|
||||
|
||||
// WHEN parsing the file
|
||||
routes, err := c.parser.ParseFile(filePath)
|
||||
|
||||
// THEN it should succeed and return the route definition
|
||||
assert.NoError(t, err, "ParseFile should not error")
|
||||
assert.Len(t, routes, 1, "Should find exactly one route")
|
||||
|
||||
route := routes[0]
|
||||
assert.Equal(t, "UserController", route.StructName, "Should extract correct struct name")
|
||||
assert.Equal(t, "/users", route.Path, "Should extract correct path")
|
||||
assert.Contains(t, route.Methods, "GET", "Should include GET method")
|
||||
assert.Empty(t, route.Parameters, "Basic route should have no parameters")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testParseFileWithParameters(t *testing.T) {
|
||||
// GIVEN a Go file with route containing parameters
|
||||
filePath := "testdata/params_route.go"
|
||||
|
||||
// WHEN parsing the file
|
||||
routes, err := c.parser.ParseFile(filePath)
|
||||
|
||||
// THEN it should succeed and extract parameters
|
||||
assert.NoError(t, err, "ParseFile should not error")
|
||||
assert.Len(t, routes, 1, "Should find exactly one route")
|
||||
|
||||
route := routes[0]
|
||||
assert.NotEmpty(t, route.Parameters, "Route should have parameters")
|
||||
|
||||
// Verify path parameter
|
||||
pathParam := findParameterByPosition(route.Parameters, "path")
|
||||
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(route.Parameters, "query")
|
||||
assert.NotNil(t, queryParam, "Should find query parameter")
|
||||
assert.Equal(t, "limit", queryParam.Name, "Query parameter should be named 'limit'")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testParseFileInvalidSyntax(t *testing.T) {
|
||||
// GIVEN a Go file with invalid syntax
|
||||
filePath := "testdata/invalid_syntax.go"
|
||||
|
||||
// WHEN parsing the file
|
||||
routes, err := c.parser.ParseFile(filePath)
|
||||
|
||||
// THEN it should fail with appropriate error
|
||||
assert.Error(t, err, "ParseFile should error on invalid syntax")
|
||||
assert.Empty(t, routes, "Should return no routes on error")
|
||||
|
||||
// Verify diagnostic information
|
||||
diagnostics := c.parser.GetDiagnostics()
|
||||
assert.NotEmpty(t, diagnostics, "Should provide diagnostic information")
|
||||
assert.Contains(t, diagnostics[0].Message, "syntax", "Error message should mention syntax")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testParseFileNonexistentFile(t *testing.T) {
|
||||
// GIVEN a nonexistent file path
|
||||
filePath := "testdata/nonexistent.go"
|
||||
|
||||
// WHEN parsing the file
|
||||
routes, err := c.parser.ParseFile(filePath)
|
||||
|
||||
// THEN it should fail with file not found error
|
||||
assert.Error(t, err, "ParseFile should error on nonexistent file")
|
||||
assert.Empty(t, routes, "Should return no routes on error")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testParseStringSimpleRoute(t *testing.T) {
|
||||
// GIVEN Go code string with simple route
|
||||
code := `
|
||||
package main
|
||||
|
||||
// @Router /users [get]
|
||||
type UserController struct {}
|
||||
`
|
||||
|
||||
// WHEN parsing the string
|
||||
routes, err := c.parser.ParseString(code)
|
||||
|
||||
// THEN it should succeed and return the route
|
||||
assert.NoError(t, err, "ParseString should not error")
|
||||
assert.Len(t, routes, 1, "Should find exactly one route")
|
||||
|
||||
route := routes[0]
|
||||
assert.Equal(t, "UserController", route.StructName, "Should extract correct struct name")
|
||||
assert.Equal(t, "/users", route.Path, "Should extract correct path")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testParseStringMultipleRoutes(t *testing.T) {
|
||||
// GIVEN Go code string with multiple routes
|
||||
code := `
|
||||
package main
|
||||
|
||||
// @Router /users [get]
|
||||
type UserController struct {}
|
||||
|
||||
// @Router /products [post]
|
||||
type ProductController struct {}
|
||||
`
|
||||
|
||||
// WHEN parsing the string
|
||||
routes, err := c.parser.ParseString(code)
|
||||
|
||||
// THEN it should succeed and return all routes
|
||||
assert.NoError(t, err, "ParseString should not error")
|
||||
assert.Len(t, routes, 2, "Should find exactly two routes")
|
||||
|
||||
// Verify both routes are found
|
||||
routeNames := []string{routes[0].StructName, routes[1].StructName}
|
||||
assert.Contains(t, routeNames, "UserController", "Should find UserController")
|
||||
assert.Contains(t, routeNames, "ProductController", "Should find ProductController")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testParseDirEmptyDirectory(t *testing.T) {
|
||||
// GIVEN an empty directory
|
||||
dirPath := "testdata/empty"
|
||||
|
||||
// WHEN parsing the directory
|
||||
routes, err := c.parser.ParseDir(dirPath)
|
||||
|
||||
// THEN it should succeed with no routes
|
||||
assert.NoError(t, err, "ParseDir should not error on empty directory")
|
||||
assert.Empty(t, routes, "Should return no routes from empty directory")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testConfiguration(t *testing.T) {
|
||||
// GIVEN default configuration
|
||||
config := c.parser.GetConfig()
|
||||
assert.NotNil(t, config, "Should have default configuration")
|
||||
|
||||
// WHEN setting new configuration
|
||||
newConfig := &RouteParserConfig{
|
||||
StrictMode: true,
|
||||
ParseComments: false,
|
||||
SourceLocations: true,
|
||||
}
|
||||
|
||||
err := c.parser.SetConfig(newConfig)
|
||||
|
||||
// THEN configuration should be updated
|
||||
assert.NoError(t, err, "SetConfig should not error")
|
||||
|
||||
updatedConfig := c.parser.GetConfig()
|
||||
assert.True(t, updatedConfig.StrictMode, "StrictMode should be updated")
|
||||
assert.False(t, updatedConfig.ParseComments, "ParseComments should be updated")
|
||||
assert.True(t, updatedConfig.SourceLocations, "SourceLocations should be updated")
|
||||
}
|
||||
|
||||
func (c *RouteParserContract) testDiagnostics(t *testing.T) {
|
||||
// GIVEN a file with warnings
|
||||
filePath := "testdata/warnings.go"
|
||||
|
||||
// WHEN parsing the file
|
||||
_, err := c.parser.ParseFile(filePath)
|
||||
|
||||
// THEN diagnostics should be available
|
||||
diagnostics := c.parser.GetDiagnostics()
|
||||
assert.NotEmpty(t, diagnostics, "Should provide diagnostic information")
|
||||
|
||||
// Verify diagnostic structure
|
||||
for _, diag := range diagnostics {
|
||||
assert.NotEmpty(t, diag.Level, "Diagnostic should have level")
|
||||
assert.NotEmpty(t, diag.Message, "Diagnostic should have message")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func findParameterByPosition(params []ParamDefinition, position string) *ParamDefinition {
|
||||
for _, param := range params {
|
||||
if param.Position == position {
|
||||
return ¶m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
387
specs/002-refactor-ast-gen/data-model.md
Normal file
387
specs/002-refactor-ast-gen/data-model.md
Normal file
@@ -0,0 +1,387 @@
|
||||
# Phase 1: Data Model Design
|
||||
|
||||
## Overview
|
||||
This document defines the core data models and contracts for the refactored AST route generation system, following the component-based architecture pattern established in the provider module.
|
||||
|
||||
## Core Entities
|
||||
|
||||
### 1. RouteDefinition
|
||||
Represents a complete route definition extracted from source code annotations.
|
||||
|
||||
```go
|
||||
type RouteDefinition struct {
|
||||
// Basic Information
|
||||
StructName string // Name of the handler struct
|
||||
FilePath string // Source file path
|
||||
PackageName string // Go package name
|
||||
|
||||
// Route Configuration
|
||||
Path string // HTTP path pattern
|
||||
Methods []string // HTTP methods (GET, POST, etc.)
|
||||
Name string // Route name for identification
|
||||
|
||||
// Dependencies & Imports
|
||||
Imports map[string]string // Package path -> alias mapping
|
||||
Parameters []ParamDefinition // Route parameters
|
||||
Middleware []string // Middleware chain
|
||||
|
||||
// Code Generation
|
||||
HandlerFunc string // Handler function name
|
||||
ReturnType string // Return type specification
|
||||
ProviderGroup string // Dependency injection group
|
||||
|
||||
// Metadata
|
||||
Location SourceLocation // Source location for error reporting
|
||||
Annotations map[string]string // Additional annotations
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ParamDefinition
|
||||
Represents a parameter binding from different sources.
|
||||
|
||||
```go
|
||||
type ParamDefinition struct {
|
||||
// Parameter Identification
|
||||
Name string // Parameter name
|
||||
Position ParamPosition // Parameter location (path, query, etc.)
|
||||
Type string // Parameter type
|
||||
Source string // Source annotation text
|
||||
|
||||
// Type Information
|
||||
BaseType string // Base type without pointer
|
||||
IsPointer bool // Is pointer type
|
||||
IsSlice bool // Is slice type
|
||||
IsMap bool // Is map type
|
||||
|
||||
// Model Information (for structured parameters)
|
||||
ModelName string // Model name for model() binding
|
||||
ModelField string // Target field in model
|
||||
ModelType string // Model field type
|
||||
|
||||
// Validation & Constraints
|
||||
Required bool // Is required parameter
|
||||
DefaultValue string // Default value
|
||||
Validation []ValidationRule // Validation rules
|
||||
|
||||
// Code Generation
|
||||
ParamToken string // Template token for generation
|
||||
ImportPath string // Import path for type
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ParamPosition
|
||||
Parameter position enumeration.
|
||||
|
||||
```go
|
||||
type ParamPosition string
|
||||
|
||||
const (
|
||||
ParamPositionPath ParamPosition = "path" // URL path parameters
|
||||
ParamPositionQuery ParamPosition = "query" // Query string parameters
|
||||
ParamPositionBody ParamPosition = "body" // Request body
|
||||
ParamPositionHeader ParamPosition = "header" // HTTP headers
|
||||
ParamPositionCookie ParamPosition = "cookie" // Cookies
|
||||
ParamPositionLocal ParamPosition = "local" // Local context values
|
||||
ParamPositionFile ParamPosition = "file" // File uploads
|
||||
)
|
||||
```
|
||||
|
||||
### 4. ValidationRule
|
||||
Parameter validation rule definition.
|
||||
|
||||
```go
|
||||
type ValidationRule struct {
|
||||
Type ValidationType // Validation type (required, min, max, etc.)
|
||||
Value string // Validation value
|
||||
Message string // Error message
|
||||
Constraint string // Constraint expression
|
||||
}
|
||||
```
|
||||
|
||||
### 5. SourceLocation
|
||||
Source code location information.
|
||||
|
||||
```go
|
||||
type SourceLocation struct {
|
||||
File string // Source file path
|
||||
Line int // Line number
|
||||
Column int // Column position
|
||||
}
|
||||
```
|
||||
|
||||
## Component Interfaces
|
||||
|
||||
### 1. RouteParser Interface
|
||||
Main coordinator interface for route parsing.
|
||||
|
||||
```go
|
||||
type RouteParser interface {
|
||||
ParseFile(filePath string) ([]RouteDefinition, error)
|
||||
ParseDir(dirPath string) ([]RouteDefinition, error)
|
||||
ParseString(code string) ([]RouteDefinition, error)
|
||||
|
||||
// Configuration
|
||||
SetConfig(config *RouteParserConfig) error
|
||||
GetConfig() *RouteParserConfig
|
||||
|
||||
// Context & Diagnostics
|
||||
GetContext() *ParserContext
|
||||
GetDiagnostics() []Diagnostic
|
||||
}
|
||||
```
|
||||
|
||||
### 2. CommentParser Interface
|
||||
Handles annotation parsing from source comments.
|
||||
|
||||
```go
|
||||
type CommentParser interface {
|
||||
ParseRouteComment(comment string) (*RouteAnnotation, error)
|
||||
ParseBindComment(comment string) (*BindAnnotation, error)
|
||||
IsRouteComment(comment string) bool
|
||||
IsBindComment(comment string) bool
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ImportResolver Interface
|
||||
Manages import resolution and dependencies.
|
||||
|
||||
```go
|
||||
type ImportResolver interface {
|
||||
ResolveFileImports(node *ast.File, filePath string) (*ImportContext, error)
|
||||
ResolveImportPath(alias string) (string, error)
|
||||
AddImport(imports map[string]string, path, alias string) error
|
||||
}
|
||||
```
|
||||
|
||||
### 4. RouteBuilder Interface
|
||||
Constructs route definitions from parsed components.
|
||||
|
||||
```go
|
||||
type RouteBuilder interface {
|
||||
BuildFromTypeSpec(typeSpec *ast.TypeSpec, decl *ast.GenDecl, context *BuilderContext) (RouteDefinition, error)
|
||||
BuildFromComment(comment string, context *BuilderContext) (RouteDefinition, error)
|
||||
ValidateDefinition(def *RouteDefinition) error
|
||||
}
|
||||
```
|
||||
|
||||
### 5. RouteValidator Interface
|
||||
Validates route definitions and configurations.
|
||||
|
||||
```go
|
||||
type RouteValidator interface {
|
||||
ValidateRoute(def *RouteDefinition) error
|
||||
ValidateParameters(params []ParamDefinition) error
|
||||
ValidateImports(imports map[string]string) error
|
||||
GetValidationRules() []ValidationRule
|
||||
}
|
||||
```
|
||||
|
||||
### 6. RouteRenderer Interface
|
||||
Handles template rendering and code generation.
|
||||
|
||||
```go
|
||||
type RouteRenderer interface {
|
||||
Render(routes []RouteDefinition, outputPath string) error
|
||||
RenderToFile(route RouteDefinition, outputPath string) error
|
||||
SetTemplate(template *template.Template) error
|
||||
GetTemplate() *template.Template
|
||||
}
|
||||
```
|
||||
|
||||
## Supporting Data Structures
|
||||
|
||||
### 1. RouteAnnotation
|
||||
Parsed route annotation information.
|
||||
|
||||
```go
|
||||
type RouteAnnotation struct {
|
||||
Path string // Route path
|
||||
Methods []string // HTTP methods
|
||||
Name string // Route name
|
||||
Options map[string]string // Additional options
|
||||
}
|
||||
```
|
||||
|
||||
### 2. BindAnnotation
|
||||
Parsed bind annotation information.
|
||||
|
||||
```go
|
||||
type BindAnnotation struct {
|
||||
Name string // Parameter name
|
||||
Position ParamPosition // Parameter position
|
||||
Key string // Source key
|
||||
Model *ModelInfo // Model binding info (optional)
|
||||
Options map[string]string // Additional options
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ModelInfo
|
||||
Model binding information.
|
||||
|
||||
```go
|
||||
type ModelInfo struct {
|
||||
Name string // Model name
|
||||
Field string // Target field
|
||||
Type string // Field type
|
||||
Required bool // Is required
|
||||
}
|
||||
```
|
||||
|
||||
### 4. ImportContext
|
||||
Import resolution context.
|
||||
|
||||
```go
|
||||
type ImportContext struct {
|
||||
FileImports map[string]*ImportResolution // Alias -> Resolution
|
||||
ImportPaths map[string]string // Path -> Alias
|
||||
ModuleInfo map[string]string // Module path -> module name
|
||||
WorkingDir string // Current working directory
|
||||
ModuleName string // Current module name
|
||||
ProcessedFiles map[string]bool // Track processed files
|
||||
}
|
||||
```
|
||||
|
||||
### 5. ImportResolution
|
||||
Individual import resolution information.
|
||||
|
||||
```go
|
||||
type ImportResolution struct {
|
||||
Path string // Import path
|
||||
Alias string // Import alias
|
||||
Type ImportType // Import type
|
||||
Used bool // Whether import is used
|
||||
}
|
||||
```
|
||||
|
||||
### 6. BuilderContext
|
||||
Context for route building process.
|
||||
|
||||
```go
|
||||
type BuilderContext struct {
|
||||
FilePath string // Current file path
|
||||
PackageName string // Package name
|
||||
ImportContext *ImportContext // Import information
|
||||
ASTFile *ast.File // AST node
|
||||
ProcessedTypes map[string]bool // Processed types cache
|
||||
Errors []error // Error collection
|
||||
Warnings []string // Warning collection
|
||||
Config *BuilderConfig // Builder configuration
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Structures
|
||||
|
||||
### 1. RouteParserConfig
|
||||
Configuration for route parser behavior.
|
||||
|
||||
```go
|
||||
type RouteParserConfig struct {
|
||||
// Parsing Options
|
||||
ParseComments bool // Parse comments (default: true)
|
||||
StrictMode bool // Strict validation mode
|
||||
SourceLocations bool // Include source locations
|
||||
|
||||
// File Processing
|
||||
SkipTestFiles bool // Skip test files (default: true)
|
||||
SkipGenerated bool // Skip generated files (default: true)
|
||||
AllowedPatterns []string // Allowed file patterns
|
||||
|
||||
// Validation Options
|
||||
EnableValidation bool // Enable validation (default: true)
|
||||
ValidationLevel ValidationLevel // Validation strictness
|
||||
|
||||
// Performance Options
|
||||
CacheEnabled bool // Enable parsing cache
|
||||
ParallelProcessing bool // Enable parallel processing
|
||||
}
|
||||
```
|
||||
|
||||
### 2. BuilderConfig
|
||||
Configuration for route builder.
|
||||
|
||||
```go
|
||||
type BuilderConfig struct {
|
||||
EnableValidation bool // Enable validation
|
||||
StrictMode bool // Strict validation mode
|
||||
DefaultParamPosition ParamPosition // Default parameter position
|
||||
AutoGenerateReturnTypes bool // Auto-generate return types
|
||||
ResolveImportDependencies bool // Resolve import dependencies
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ValidationLevel
|
||||
Validation strictness levels.
|
||||
|
||||
```go
|
||||
type ValidationLevel int
|
||||
|
||||
const (
|
||||
ValidationLevelNone ValidationLevel = iota // No validation
|
||||
ValidationLevelBasic // Basic validation
|
||||
ValidationLevelStrict // Strict validation
|
||||
ValidationLevelPedantic // Pedantic validation
|
||||
)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 1. RouteError
|
||||
Route-specific error type.
|
||||
|
||||
```go
|
||||
type RouteError struct {
|
||||
Code ErrorCode // Error code
|
||||
Message string // Error message
|
||||
File string // File path
|
||||
Line int // Line number
|
||||
Column int // Column number
|
||||
Context string // Error context
|
||||
Severity ErrorSeverity // Error severity
|
||||
Inner error // Inner error
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ErrorCode
|
||||
Error code enumeration.
|
||||
|
||||
```go
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrCodeInvalidSyntax ErrorCode = "INVALID_SYNTAX"
|
||||
ErrCodeMissingAnnotation ErrorCode = "MISSING_ANNOTATION"
|
||||
ErrCodeInvalidParameter ErrorCode = "INVALID_PARAMETER"
|
||||
ErrCodeDuplicateRoute ErrorCode = "DUPLICATE_ROUTE"
|
||||
ErrCodeImportResolution ErrorCode = "IMPORT_RESOLUTION"
|
||||
ErrCodeValidation ErrorCode = "VALIDATION_ERROR"
|
||||
ErrCodeTemplateError ErrorCode = "TEMPLATE_ERROR"
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Diagnostic
|
||||
Rich diagnostic information.
|
||||
|
||||
```go
|
||||
type Diagnostic struct {
|
||||
Level DiagnosticLevel // Diagnostic level
|
||||
Code ErrorCode // Error code
|
||||
Message string // Diagnostic message
|
||||
File string // File path
|
||||
Location SourceLocation // Source location
|
||||
Context string // Additional context
|
||||
Suggestions []string // Suggested fixes
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
This data model design provides a comprehensive foundation for the refactored route generation system. Key features include:
|
||||
|
||||
1. **Clear separation of concerns**: Each component has well-defined interfaces and responsibilities
|
||||
2. **Comprehensive error handling**: Structured error types with rich diagnostic information
|
||||
3. **Extensible validation**: Configurable validation system with multiple levels
|
||||
4. **Type safety**: Strong typing throughout the system
|
||||
5. **Configuration management**: Flexible configuration system for different use cases
|
||||
6. **Backward compatibility**: Designed to support existing annotation formats
|
||||
|
||||
The design follows SOLID principles and provides a solid foundation for implementing the refactored route generation system.
|
||||
254
specs/002-refactor-ast-gen/plan.md
Normal file
254
specs/002-refactor-ast-gen/plan.md
Normal file
@@ -0,0 +1,254 @@
|
||||
|
||||
# Implementation Plan: Refactor AST Generation Routes Workflow
|
||||
|
||||
**Branch**: `002-refactor-ast-gen` | **Date**: 2025-09-22 | **Spec**: [/specs/002-refactor-ast-gen/spec.md](/specs/002-refactor-ast-gen/spec.md)
|
||||
**Input**: Feature specification from `/specs/002-refactor-ast-gen/spec.md`
|
||||
**User Requirements**: 1. 重构 @pkg/ast/route/ 的实现流程,使更易读,逻辑更清晰,2.保证 @cmd/gen_route.go 对重构后方法调用的生效,3. 一切功能重构保证测试优先。
|
||||
|
||||
## Execution Flow (/plan command scope)
|
||||
```
|
||||
1. Load feature spec from Input path
|
||||
→ If not found: ERROR "No feature spec at {path}"
|
||||
2. Fill Technical Context (scan for NEEDS CLARIFICATION)
|
||||
→ Detect Project Type from context (web=frontend+backend, mobile=app+api)
|
||||
→ Set Structure Decision based on project type
|
||||
3. Fill the Constitution Check section based on the content of the constitution document.
|
||||
4. Evaluate Constitution Check section below
|
||||
→ If violations exist: Document in Complexity Tracking
|
||||
→ If no justification possible: ERROR "Simplify approach first"
|
||||
→ Update Progress Tracking: Initial Constitution Check
|
||||
5. Execute Phase 0 → research.md
|
||||
→ If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns"
|
||||
6. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, `GEMINI.md` for Gemini CLI, `QWEN.md` for Qwen Code or `AGENTS.md` for opencode).
|
||||
7. Re-evaluate Constitution Check section
|
||||
→ If new violations: Refactor design, return to Phase 1
|
||||
→ Update Progress Tracking: Post-Design Constitution Check
|
||||
8. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md)
|
||||
9. STOP - Ready for /tasks command
|
||||
```
|
||||
|
||||
**IMPORTANT**: The /plan command STOPS at step 7. Phases 2-4 are executed by other commands:
|
||||
- Phase 2: /tasks command creates tasks.md
|
||||
- Phase 3-4: Implementation execution (manual or via tools)
|
||||
|
||||
## Summary
|
||||
重构AST生成路由工作流程,提高代码可读性和逻辑清晰度,确保与现有gen_route.go命令的兼容性,并采用测试驱动开发方法。
|
||||
|
||||
## Technical Context
|
||||
**Language/Version**: Go 1.21+
|
||||
**Primary Dependencies**: go standard library (ast, parser, token), cobra CLI, logrus
|
||||
**Storage**: File-based route definitions and generated Go code
|
||||
**Testing**: Go testing with TDD approach (testing/fstest for filesystem tests)
|
||||
**Target Platform**: CLI tool for Go projects
|
||||
**Project Type**: Single project with existing pkg/ast/provider refactoring patterns
|
||||
**Performance Goals**: Fast parsing (< 2s for typical project), minimal memory overhead
|
||||
**Constraints**: Must maintain backward compatibility with existing @Router and @Bind annotations
|
||||
**Scale/Scope**: Support medium to large Go projects with extensive route definitions
|
||||
|
||||
## Constitution Check
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
### SOLID Principles Compliance
|
||||
- [x] **Single Responsibility**: Route parsing, generation, and rendering will have separate, focused components
|
||||
- [x] **Open/Closed**: Design will follow existing provider patterns with extensible interfaces
|
||||
- [x] **Liskov Substitution**: New route components will implement consistent interfaces
|
||||
- [x] **Interface Segregation**: Specific interfaces for parsing, generation, and validation
|
||||
- [x] **Dependency Inversion**: Core functionality will depend on interfaces, not concrete implementations
|
||||
|
||||
### KISS Principle Compliance
|
||||
- [x] Design avoids unnecessary complexity - will follow existing refactored provider patterns
|
||||
- [x] CLI interface maintains consistency - existing gen_route.go interface preserved
|
||||
- [x] Code generation logic is simple and direct - clear separation of concerns
|
||||
- [x] Solutions are intuitive and easy to understand - follows established patterns
|
||||
|
||||
### YAGNI Principle Compliance
|
||||
- [x] Only implementing clearly needed functionality - focus on readability and clarity improvements
|
||||
- [x] No over-engineering or future-proofing without requirements - minimal changes to achieve goals
|
||||
- [x] Each feature has explicit user requirements - based on gen_route.go compatibility needs
|
||||
- [x] No "might be useful" features without justification - scope limited to refactoring
|
||||
|
||||
### DRY Principle Compliance
|
||||
- [x] No code duplication across components - will share patterns with pkg/ast/provider
|
||||
- [x] Common functionality is abstracted and reused - leverage existing interfaces and utilities
|
||||
- [x] Template system avoids repetitive implementations - consistent with provider generation
|
||||
- [x] Shared utilities are properly abstracted - reuse existing AST parsing infrastructure
|
||||
|
||||
### Code Quality Standards
|
||||
- [x] **Testing Discipline**: TDD approach with Red-Green-Refactor cycle - testing first requirement
|
||||
- [x] **CLI Consistency**: Unified parameter formats and output standards - existing interface maintained
|
||||
- [x] **Error Handling**: Complete error information and recovery mechanisms - consistent with provider patterns
|
||||
- [x] **Performance**: Generation speed and memory usage requirements met - <2s parsing goal
|
||||
|
||||
### Complexity Tracking
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| [Document any deviations from constitutional principles] | [Justification for complexity] | [Why simpler approach insufficient] |
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
```
|
||||
specs/[###-feature]/
|
||||
├── plan.md # This file (/plan command output)
|
||||
├── research.md # Phase 0 output (/plan command)
|
||||
├── data-model.md # Phase 1 output (/plan command)
|
||||
├── quickstart.md # Phase 1 output (/plan command)
|
||||
├── contracts/ # Phase 1 output (/plan command)
|
||||
└── tasks.md # Phase 2 output (/tasks command - NOT created by /plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
```
|
||||
# Option 1: Single project (DEFAULT)
|
||||
src/
|
||||
├── models/
|
||||
├── services/
|
||||
├── cli/
|
||||
└── lib/
|
||||
|
||||
tests/
|
||||
├── contract/
|
||||
├── integration/
|
||||
└── unit/
|
||||
|
||||
# Option 2: Web application (when "frontend" + "backend" detected)
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── api/
|
||||
└── tests/
|
||||
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ └── services/
|
||||
└── tests/
|
||||
|
||||
# Option 3: Mobile + API (when "iOS/Android" detected)
|
||||
api/
|
||||
└── [same as backend above]
|
||||
|
||||
ios/ or android/
|
||||
└── [platform-specific structure]
|
||||
```
|
||||
|
||||
**Structure Decision**: Option 1 - Single project with pkg/ast/route refactoring following pkg/ast/provider patterns
|
||||
|
||||
## Phase 0: Outline & Research
|
||||
1. **Extract unknowns from Technical Context** above:
|
||||
- For each NEEDS CLARIFICATION → research task
|
||||
- For each dependency → best practices task
|
||||
- For each integration → patterns task
|
||||
|
||||
2. **Generate and dispatch research agents**:
|
||||
```
|
||||
For each unknown in Technical Context:
|
||||
Task: "Research {unknown} for {feature context}"
|
||||
For each technology choice:
|
||||
Task: "Find best practices for {tech} in {domain}"
|
||||
```
|
||||
|
||||
3. **Consolidate findings** in `research.md` using format:
|
||||
- Decision: [what was chosen]
|
||||
- Rationale: [why chosen]
|
||||
- Alternatives considered: [what else evaluated]
|
||||
|
||||
**Output**: research.md with all NEEDS CLARIFICATION resolved
|
||||
|
||||
## Phase 1: Design & Contracts
|
||||
*Prerequisites: research.md complete*
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships
|
||||
- Validation rules from requirements
|
||||
- State transitions if applicable
|
||||
|
||||
2. **Generate API contracts** from functional requirements:
|
||||
- For each user action → endpoint
|
||||
- Use standard REST/GraphQL patterns
|
||||
- Output OpenAPI/GraphQL schema to `/contracts/`
|
||||
|
||||
3. **Generate contract tests** from contracts:
|
||||
- One test file per endpoint
|
||||
- Assert request/response schemas
|
||||
- Tests must fail (no implementation yet)
|
||||
|
||||
4. **Extract test scenarios** from user stories:
|
||||
- Each story → integration test scenario
|
||||
- Quickstart test = story validation steps
|
||||
|
||||
5. **Update agent file incrementally** (O(1) operation):
|
||||
- Run `.specify/scripts/bash/update-agent-context.sh claude` for your AI assistant
|
||||
- If exists: Add only NEW tech from current plan
|
||||
- Preserve manual additions between markers
|
||||
- Update recent changes (keep last 3)
|
||||
- Keep under 150 lines for token efficiency
|
||||
- Output to repository root
|
||||
|
||||
**Output**: data-model.md, /contracts/*, failing tests, quickstart.md, agent-specific file
|
||||
|
||||
## Phase 2: Task Planning Approach
|
||||
*This section describes what the /tasks command will do - DO NOT execute during /plan*
|
||||
|
||||
**Task Generation Strategy**:
|
||||
- Load `.specify/templates/tasks-template.md` as base
|
||||
- Generate tasks from Phase 1 design docs (contracts, data model, quickstart)
|
||||
- Each contract → contract test implementation task [P]
|
||||
- Each data model entity → implementation task [P]
|
||||
- Each interface → component implementation task
|
||||
- Integration tasks to ensure compatibility with gen_route.go
|
||||
- Test-driven implementation following TDD principles
|
||||
|
||||
**Ordering Strategy**:
|
||||
- TDD order: Write failing tests first, then implement to make tests pass
|
||||
- Component dependency order: Core interfaces → Parsers → Builders → Validators → Renderers
|
||||
- Backward compatibility: Ensure gen_route.go works throughout implementation
|
||||
- Mark [P] for parallel execution (independent components)
|
||||
|
||||
**Estimated Output**: 25-30 numbered, ordered tasks in tasks.md covering:
|
||||
- Core interface implementations
|
||||
- Data model and error handling
|
||||
- Route parsing and validation
|
||||
- Template rendering and code generation
|
||||
- Integration and compatibility testing
|
||||
- Performance and validation testing
|
||||
|
||||
**IMPORTANT**: This phase is executed by the /tasks command, NOT by /plan
|
||||
|
||||
## Phase 3+: Future Implementation
|
||||
*These phases are beyond the scope of the /plan command*
|
||||
|
||||
**Phase 3**: Task execution (/tasks command creates tasks.md)
|
||||
**Phase 4**: Implementation (execute tasks.md following constitutional principles)
|
||||
**Phase 5**: Validation (run tests, execute quickstart.md, performance validation)
|
||||
|
||||
## Complexity Tracking
|
||||
*Fill ONLY if Constitution Check has violations that must be justified*
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
|
||||
|
||||
## Progress Tracking
|
||||
*This checklist is updated during execution flow*
|
||||
|
||||
**Phase Status**:
|
||||
- [x] Phase 0: Research complete (/plan command)
|
||||
- [x] Phase 1: Design complete (/plan command)
|
||||
- [x] Phase 2: Task planning complete (/plan command - describe approach only)
|
||||
- [ ] Phase 3: Tasks generated (/tasks command)
|
||||
- [ ] Phase 4: Implementation complete
|
||||
- [ ] Phase 5: Validation passed
|
||||
|
||||
**Gate Status**:
|
||||
- [x] Initial Constitution Check: PASS
|
||||
- [x] Post-Design Constitution Check: PASS
|
||||
- [x] All NEEDS CLARIFICATION resolved
|
||||
- [x] Complexity deviations documented
|
||||
|
||||
---
|
||||
*Based on Constitution v1.0.0 - See `/memory/constitution.md`*
|
||||
353
specs/002-refactor-ast-gen/quickstart.md
Normal file
353
specs/002-refactor-ast-gen/quickstart.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# Phase 1: Quickstart Guide
|
||||
|
||||
## Overview
|
||||
This quickstart guide demonstrates how to use the refactored AST route generation system. It covers the basic workflow for defining routes and generating route handler code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.21 or higher
|
||||
- Existing atomctl project structure
|
||||
- Basic understanding of Go annotations
|
||||
|
||||
## Basic Route Definition
|
||||
|
||||
### 1. Simple Route
|
||||
|
||||
Create a controller with basic route annotation:
|
||||
|
||||
```go
|
||||
// app/http/user_controller.go
|
||||
package http
|
||||
|
||||
// @Router /users [get]
|
||||
type UserController struct {}
|
||||
```
|
||||
|
||||
Generate routes:
|
||||
```bash
|
||||
atomctl gen route
|
||||
```
|
||||
|
||||
### 2. Route with Parameters
|
||||
|
||||
Add parameter bindings using `@Bind` annotations:
|
||||
|
||||
```go
|
||||
// app/http/user_controller.go
|
||||
package http
|
||||
|
||||
// @Router /users/:id [get]
|
||||
// @Bind id (path) model()
|
||||
// @Bind limit (query) model(limit:int)
|
||||
type UserController struct {}
|
||||
```
|
||||
|
||||
## Parameter Binding Types
|
||||
|
||||
### Path Parameters
|
||||
```go
|
||||
// @Bind id (path) model()
|
||||
// @Bind name (path) model(name:string)
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
```go
|
||||
// @Bind limit (query) model(limit:int)
|
||||
// @Bind offset (query) model(offset:int)
|
||||
// @Bind filter (query)
|
||||
```
|
||||
|
||||
### Body Parameters
|
||||
```go
|
||||
// @Bind user (body) model(User)
|
||||
// @Bind data (body) model(CreateUserRequest)
|
||||
```
|
||||
|
||||
### Header Parameters
|
||||
```go
|
||||
// @Bind authorization (header)
|
||||
// @Bind x-api-key (header) model(APIKey)
|
||||
```
|
||||
|
||||
## Generated Code Structure
|
||||
|
||||
The route generation will create a `routes.gen.go` file:
|
||||
|
||||
```go
|
||||
// app/http/routes.gen.go
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/http"
|
||||
"go.ipao.vip/gen/model"
|
||||
)
|
||||
|
||||
type RouteProvider struct {
|
||||
userController *UserController
|
||||
}
|
||||
|
||||
func (p *RouteProvider) Provide(opts ...contracts.Option) error {
|
||||
// Route registration logic here
|
||||
p.userController = &UserController{}
|
||||
|
||||
// Register /users route
|
||||
http.Handle("/users", p.userController.GetUsers)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserController method stubs
|
||||
func (c *UserController) GetUsers(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Generated method implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Your Routes
|
||||
|
||||
### 1. Unit Test
|
||||
```go
|
||||
// app/http/user_controller_test.go
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserController_GetUsers(t *testing.T) {
|
||||
controller := &UserController{}
|
||||
|
||||
req := httptest.NewRequest("GET", "/users", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
controller.GetUsers(context.Background(), w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Integration Test
|
||||
```go
|
||||
// integration/user_routes_test.go
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserRoutes(t *testing.T) {
|
||||
// Setup router with generated routes
|
||||
router := setupRouter()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
method string
|
||||
wantStatus int
|
||||
}{
|
||||
{"Get Users", "/users", "GET", http.StatusOK},
|
||||
{"Get User by ID", "/users/123", "GET", http.StatusOK},
|
||||
{"Create User", "/users", "POST", http.StatusCreated},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tt.method, tt.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, tt.wantStatus, w.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### 1. Route Groups
|
||||
```go
|
||||
// @Router /api/v1/users [get]
|
||||
// @Router /api/v1/users/:id [get,put,delete]
|
||||
type UserController struct {}
|
||||
```
|
||||
|
||||
### 2. Middleware Integration
|
||||
```go
|
||||
// @Router /admin [get]
|
||||
// @Middleware auth,admin
|
||||
type AdminController struct {}
|
||||
```
|
||||
|
||||
### 3. Custom Return Types
|
||||
```go
|
||||
// @Router /users [post]
|
||||
// @ReturnType UserResponse
|
||||
type UserController struct {}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Parser Configuration
|
||||
```go
|
||||
config := &route.RouteParserConfig{
|
||||
StrictMode: true,
|
||||
ParseComments: true,
|
||||
SourceLocations: true,
|
||||
EnableValidation: true,
|
||||
}
|
||||
```
|
||||
|
||||
### Builder Configuration
|
||||
```go
|
||||
config := &route.BuilderConfig{
|
||||
EnableValidation: true,
|
||||
StrictMode: true,
|
||||
DefaultParamPosition: route.ParamPositionQuery,
|
||||
AutoGenerateReturnTypes: true,
|
||||
ResolveImportDependencies: true,
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 1. Validation Errors
|
||||
The refactored system provides detailed error messages:
|
||||
|
||||
```bash
|
||||
$ atomctl gen route
|
||||
Error: invalid route syntax in user_controller.go:15
|
||||
Expected: @Router /path [method]
|
||||
Found: @Router /users
|
||||
Fix: Add HTTP methods in brackets
|
||||
```
|
||||
|
||||
### 2. Parameter Binding Errors
|
||||
```bash
|
||||
$ atomctl gen route
|
||||
Error: invalid parameter binding in user_controller.go:16
|
||||
Parameter 'id' has invalid position 'invalid'
|
||||
Valid positions: path, query, body, header, cookie, local, file
|
||||
```
|
||||
|
||||
## Migration from Legacy System
|
||||
|
||||
### 1. Existing Code Compatibility
|
||||
The refactored system maintains full backward compatibility:
|
||||
|
||||
```go
|
||||
// This still works
|
||||
// @Router /users [get]
|
||||
// @Bind id (path) model()
|
||||
type UserController struct {}
|
||||
```
|
||||
|
||||
### 2. Gradual Migration
|
||||
You can migrate files incrementally:
|
||||
|
||||
```bash
|
||||
# Generate routes for specific files
|
||||
atomctl gen route app/http/user_controller.go
|
||||
|
||||
# Or generate for entire directory
|
||||
atomctl gen route app/http/
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. Caching
|
||||
The refactored system includes caching for improved performance:
|
||||
|
||||
```go
|
||||
config := &route.RouteParserConfig{
|
||||
CacheEnabled: true,
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Parallel Processing
|
||||
Enable parallel processing for large projects:
|
||||
|
||||
```go
|
||||
config := &route.RouteParserConfig{
|
||||
ParallelProcessing: true,
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging and Diagnostics
|
||||
|
||||
### 1. Enable Detailed Logging
|
||||
```go
|
||||
config := &route.RouteParserConfig{
|
||||
SourceLocations: true,
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Access Diagnostics
|
||||
```go
|
||||
parser := route.NewRouteParser()
|
||||
routes, err := parser.ParseFile("controller.go")
|
||||
|
||||
// Get detailed diagnostics
|
||||
diagnostics := parser.GetDiagnostics()
|
||||
for _, diag := range diagnostics {
|
||||
fmt.Printf("%s: %s (%s:%d)\n", diag.Level, diag.Message, diag.File, diag.Location.Line)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Route Definition
|
||||
- Use descriptive route names
|
||||
- Group related routes together
|
||||
- Follow REST conventions where applicable
|
||||
|
||||
### 2. Parameter Binding
|
||||
- Use appropriate parameter positions
|
||||
- Provide clear parameter names
|
||||
- Add validation for complex parameters
|
||||
|
||||
### 3. Error Handling
|
||||
- Implement proper error handling in controllers
|
||||
- Use appropriate HTTP status codes
|
||||
- Provide meaningful error messages
|
||||
|
||||
### 4. Testing
|
||||
- Write comprehensive tests for all routes
|
||||
- Test both success and error scenarios
|
||||
- Use contract tests for consistency
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Routes not generated**: Check file naming and location
|
||||
2. **Parameters not parsed**: Verify annotation syntax
|
||||
3. **Import errors**: Ensure all dependencies are available
|
||||
4. **Compilation errors**: Check generated code syntax
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Review the contract tests in `contracts/` directory
|
||||
- Check the diagnostic output for detailed error information
|
||||
- Run tests to verify implementation correctness
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Define your routes using `@Router` and `@Bind` annotations
|
||||
2. Run `atomctl gen route` to generate route code
|
||||
3. Implement the generated controller methods
|
||||
4. Write tests to verify functionality
|
||||
5. Configure options as needed for your project
|
||||
|
||||
The refactored system provides a solid foundation for route generation with improved maintainability, testability, and extensibility.
|
||||
137
specs/002-refactor-ast-gen/research.md
Normal file
137
specs/002-refactor-ast-gen/research.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Phase 0: Research Findings
|
||||
|
||||
## Research Overview
|
||||
Conducted comprehensive analysis of existing `/projects/atomctl/pkg/ast/route/` implementation to understand current architecture, identify improvement opportunities, and establish refactoring strategy.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Current Architecture Analysis
|
||||
|
||||
**File Structure:**
|
||||
- `route.go` (272 lines): Core parsing logic with mixed responsibilities
|
||||
- `builder.go` (155 lines): Template data construction
|
||||
- `render.go` (54 lines): Rendering coordination entry point
|
||||
- `renderer.go` (23 lines): Template rendering wrapper
|
||||
- `router.go.tpl` (47 lines): Go template for code generation
|
||||
|
||||
**Architecture Pattern:** Monolithic design with functional approach, lacking clear component boundaries
|
||||
|
||||
### 2. Comparison with Refactored Provider Module
|
||||
|
||||
| Aspect | Current Route Module | Refactored Provider Module |
|
||||
|--------|---------------------|---------------------------|
|
||||
| **Architecture** | Monolithic, flat | Component-based, layered |
|
||||
| **Components** | 5 files, unclear boundaries | 15+ files, clear separation |
|
||||
| **Error Handling** | Simple, uses panic | Comprehensive error collection |
|
||||
| **Extensibility** | Limited | Highly extensible |
|
||||
| **Test Coverage** | Minimal | Comprehensive test strategy |
|
||||
| **Configuration** | Hardcoded | Configurable system |
|
||||
|
||||
### 3. Identified Problems
|
||||
|
||||
**Design Principle Violations:**
|
||||
- **DRY**: Duplicate import parsing and AST traversal logic with provider module
|
||||
- **SRP**: `route.go` handles parsing, validation, and construction
|
||||
- **OCP**: Adding new parameter types requires core code changes
|
||||
- **DIP**: Direct dependencies between components
|
||||
|
||||
**Code Quality Issues:**
|
||||
- Use of `panic` instead of proper error handling
|
||||
- Hardcoded paths and package names
|
||||
- Complex type judgment logic without abstraction
|
||||
- Insufficient test coverage
|
||||
|
||||
### 4. Refactoring Strategy
|
||||
|
||||
**Decision**: Adopt the successful patterns from `pkg/ast/provider` refactoring
|
||||
**Rationale**:
|
||||
- Proven architecture that SOLID principles
|
||||
- Maintains backward compatibility
|
||||
- Provides clear migration path
|
||||
- Leverages existing shared utilities
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Minimal fixes to existing code (rejected: doesn't address architectural issues)
|
||||
- Complete rewrite (rejected: too risky, breaks compatibility)
|
||||
- Incremental refactoring (selected: balances improvement and stability)
|
||||
|
||||
## Research-Driven Decisions
|
||||
|
||||
### 1. Architecture Decision
|
||||
**Decision**: Component-based architecture following provider patterns
|
||||
**Components to Create:**
|
||||
- `RouteParser` (coordinator)
|
||||
- `CommentParser` (annotation parsing)
|
||||
- `ImportResolver` (import processing)
|
||||
- `RouteBuilder` (route construction)
|
||||
- `RouteValidator` (validation logic)
|
||||
- `RouteRenderer` (template rendering)
|
||||
|
||||
### 2. Compatibility Decision
|
||||
**Decision**: Maintain full backward compatibility
|
||||
**Requirements:**
|
||||
- Preserve existing `@Router` and `@Bind` annotation syntax
|
||||
- Keep `cmd/gen_route.go` interface unchanged
|
||||
- Ensure generated code output remains identical
|
||||
|
||||
### 3. Testing Strategy Decision
|
||||
**Decision**: Test-driven development approach
|
||||
**Approach:**
|
||||
- Write comprehensive tests first (contract tests)
|
||||
- Refactor implementation to make tests pass
|
||||
- Maintain test coverage throughout refactoring
|
||||
|
||||
### 4. Performance Decision
|
||||
**Decision**: Maintain current performance characteristics
|
||||
**Targets:**
|
||||
- <2s parsing for typical projects
|
||||
- Minimal memory overhead
|
||||
- No performance regression
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Design & Contracts
|
||||
1. Define new component interfaces based on provider patterns
|
||||
2. Create data models and contracts
|
||||
3. Establish test scenarios and acceptance criteria
|
||||
|
||||
### Phase 2: Task Implementation
|
||||
1. Implement new component architecture
|
||||
2. Migrate existing logic incrementally
|
||||
3. Maintain compatibility through testing
|
||||
|
||||
### Phase 3: Validation
|
||||
1. Comprehensive testing across all scenarios
|
||||
2. Performance validation
|
||||
3. Integration testing with existing systems
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
**Low Risk:**
|
||||
- Backward compatibility maintained
|
||||
- Incremental refactoring approach
|
||||
- Proven architectural patterns
|
||||
|
||||
**Medium Risk:**
|
||||
- Complex parameter handling logic migration
|
||||
- Template system integration
|
||||
- Error handling standardization
|
||||
|
||||
**Mitigation Strategies:**
|
||||
- Comprehensive test coverage
|
||||
- Incremental implementation with validation
|
||||
- Rollback capability at each stage
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Code Quality**: Clear separation of concerns, SOLID compliance
|
||||
2. **Maintainability**: Component-based architecture with clear boundaries
|
||||
3. **Testability**: Comprehensive test coverage with clear contract tests
|
||||
4. **Compatibility**: Zero breaking changes to existing functionality
|
||||
5. **Performance**: No performance regression
|
||||
|
||||
## Conclusion
|
||||
|
||||
The research confirms that refactoring `pkg/ast/route` using the successful patterns from `pkg/ast/provider` is the optimal approach. This will improve code maintainability, testability, and extensibility while preserving all existing functionality.
|
||||
|
||||
**Decision Status**: ✅ APPROVED - Proceed to Phase 1 design
|
||||
122
specs/002-refactor-ast-gen/spec.md
Normal file
122
specs/002-refactor-ast-gen/spec.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Feature Specification: Refactor AST Generation Routes Workflow
|
||||
|
||||
**Feature Branch**: `002-refactor-ast-gen`
|
||||
**Created**: 2025-09-22
|
||||
**Status**: Draft
|
||||
**Input**: User description: "refactor ast gen routes workflow"
|
||||
|
||||
## Execution Flow (main)
|
||||
```
|
||||
1. Parse user description from Input
|
||||
<20> If empty: ERROR "No feature description provided"
|
||||
2. Extract key concepts from description
|
||||
<20> Identify: actors, actions, data, constraints
|
||||
3. For each unclear aspect:
|
||||
<20> Mark with [NEEDS CLARIFICATION: specific question]
|
||||
4. Fill User Scenarios & Testing section
|
||||
<20> If no clear user flow: ERROR "Cannot determine user scenarios"
|
||||
5. Generate Functional Requirements
|
||||
<20> Each requirement must be testable
|
||||
<20> Mark ambiguous requirements
|
||||
6. Identify Key Entities (if data involved)
|
||||
7. Run Review Checklist
|
||||
<20> If any [NEEDS CLARIFICATION]: WARN "Spec has uncertainties"
|
||||
<20> If implementation details found: ERROR "Remove tech details"
|
||||
8. Return: SUCCESS (spec ready for planning)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## <20> Quick Guidelines
|
||||
- Focus on WHAT users need and WHY
|
||||
- L Avoid HOW to implement (no tech stack, APIs, code structure)
|
||||
- =e Written for business stakeholders, not developers
|
||||
|
||||
### Section Requirements
|
||||
- **Mandatory sections**: Must be completed for every feature
|
||||
- **Optional sections**: Include only when relevant to the feature
|
||||
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
|
||||
|
||||
### For AI Generation
|
||||
When creating this spec from a user prompt:
|
||||
1. **Mark all ambiguities**: Use [NEEDS CLARIFICATION: specific question] for any assumption you'd need to make
|
||||
2. **Don't guess**: If the prompt doesn't specify something (e.g., "login system" without auth method), mark it
|
||||
3. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
|
||||
4. **Common underspecified areas**:
|
||||
- User types and permissions
|
||||
- Data retention/deletion policies
|
||||
- Performance targets and scale
|
||||
- Error handling behaviors
|
||||
- Integration requirements
|
||||
- Security/compliance needs
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### Primary User Story
|
||||
As a developer using the atomctl code generation system, I need the AST-based route generation workflow to be refactored so that it is more maintainable, extensible, and follows consistent patterns with other generation workflows in the system.
|
||||
|
||||
### Acceptance Scenarios
|
||||
1. **Given** a developer wants to generate route handlers from AST annotations, **When** they run the generation command, **Then** the system should correctly parse route definitions and generate appropriate handler code
|
||||
2. **Given** existing route generation code has inconsistent patterns, **When** the refactoring is complete, **Then** all route generation should follow the same architectural patterns as other providers
|
||||
3. **Given** the current system has duplicate logic, **When** the refactoring is complete, **Then** common functionality should be shared and DRY principles should be applied
|
||||
|
||||
### Edge Cases
|
||||
- What happens when the system encounters unsupported route annotations?
|
||||
- How does the system handle conflicting route definitions?
|
||||
- What occurs when there are circular dependencies between route handlers?
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
- **FR-001**: System MUST parse route-related annotations from AST structures
|
||||
- **FR-002**: System MUST generate route handler code based on parsed annotations
|
||||
- **FR-003**: Users MUST be able to define route patterns and HTTP methods through annotations
|
||||
- **FR-004**: System MUST integrate route generation with existing provider generation workflow
|
||||
- **FR-005**: System MUST eliminate duplicate code between route generation and other generation workflows
|
||||
- **FR-006**: System MUST follow consistent error handling patterns across all generation workflows
|
||||
- **FR-007**: System MUST provide clear feedback when route generation fails or encounters issues
|
||||
|
||||
*Example of marking unclear requirements:*
|
||||
- **FR-008**: System MUST support [NEEDS CLARIFICATION: which HTTP methods? GET, POST, PUT, DELETE, or all?]
|
||||
- **FR-009**: Route generation MUST handle [NEEDS CLARIFICATION: what level of route complexity? simple paths, parameters, wildcards?]
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
- **Route Definition**: Represents a route annotation containing path, HTTP method, and handler information
|
||||
- **Route Generator**: Component responsible for transforming route annotations into executable code
|
||||
- **Route Parser**: Component that extracts route information from AST structures
|
||||
- **Route Template**: Code generation template that produces the final route handler code
|
||||
|
||||
---
|
||||
|
||||
## Review & Acceptance Checklist
|
||||
*GATE: Automated checks run during main() execution*
|
||||
|
||||
### Content Quality
|
||||
- [ ] No implementation details (languages, frameworks, APIs)
|
||||
- [ ] Focused on user value and business needs
|
||||
- [ ] Written for non-technical stakeholders
|
||||
- [ ] All mandatory sections completed
|
||||
|
||||
### Requirement Completeness
|
||||
- [ ] No [NEEDS CLARIFICATION] markers remain
|
||||
- [ ] Requirements are testable and unambiguous
|
||||
- [ ] Success criteria are measurable
|
||||
- [ ] Scope is clearly bounded
|
||||
- [ ] Dependencies and assumptions identified
|
||||
|
||||
---
|
||||
|
||||
## Execution Status
|
||||
*Updated by main() during processing*
|
||||
|
||||
- [x] User description parsed
|
||||
- [x] Key concepts extracted
|
||||
- [x] Ambiguities marked
|
||||
- [x] User scenarios defined
|
||||
- [x] Requirements generated
|
||||
- [x] Entities identified
|
||||
- [ ] Review checklist passed
|
||||
|
||||
---
|
||||
148
specs/002-refactor-ast-gen/tasks.md
Normal file
148
specs/002-refactor-ast-gen/tasks.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Tasks: Refactor AST Generation Routes Workflow
|
||||
|
||||
**Input**: Design documents from `/specs/002-refactor-ast-gen/`
|
||||
**Prerequisites**: plan.md, research.md, data-model.md, contracts/, quickstart.md
|
||||
|
||||
## Execution Flow (main)
|
||||
```
|
||||
1. Load plan.md from feature directory
|
||||
→ Extract: Go 1.21+, ast/parser/token, cobra CLI, logrus
|
||||
→ Extract: pkg/ast/route/ refactoring following provider patterns
|
||||
2. Load design documents:
|
||||
→ data-model.md: Extract RouteDefinition, ParamDefinition, 6 interfaces
|
||||
→ contracts/: 2 contract test files → 2 contract test tasks
|
||||
→ research.md: Extract component architecture → setup tasks
|
||||
→ quickstart.md: Extract usage scenarios → integration test tasks
|
||||
3. Generate tasks by category:
|
||||
→ Setup: project structure, core interfaces, error handling
|
||||
→ Tests: contract tests, integration tests, compatibility tests
|
||||
→ Core: RouteParser, RouteBuilder, RouteValidator, RouteRenderer components
|
||||
→ Integration: compatibility layer, cmd/gen_route.go integration
|
||||
→ Polish: performance tests, documentation, validation
|
||||
4. Apply task rules:
|
||||
→ Different components = mark [P] for parallel development
|
||||
→ Same file = sequential (no [P])
|
||||
→ Tests before implementation (TDD)
|
||||
5. Number tasks sequentially (T001, T002...)
|
||||
6. Generate dependency graph
|
||||
7. Create parallel execution examples
|
||||
8. Validate task completeness:
|
||||
→ All contracts have tests?
|
||||
→ All entities have models?
|
||||
→ All components implemented?
|
||||
9. Return: SUCCESS (tasks ready for execution)
|
||||
```
|
||||
|
||||
## Format: `[ID] [P?] Description`
|
||||
- **[P]**: Can run in parallel (different components, no dependencies)
|
||||
- Include exact file paths in descriptions
|
||||
|
||||
## Phase 3.1: Setup ✅ COMPLETED
|
||||
- [x] **T001** Verify existing pkg/ast/route/ structure
|
||||
- [x] **T002** Initialize Go module dependencies for testing
|
||||
- [x] **T003** Setup linting and formatting tools configuration
|
||||
|
||||
## Phase 3.2: Tests First (TDD) ⚠️ MUST COMPLETE BEFORE 3.3
|
||||
**CRITICAL: These tests MUST be written and MUST FAIL before ANY implementation**
|
||||
|
||||
### Basic Tests ✅ COMPLETED
|
||||
- [x] **T004** Create basic route parsing test in pkg/ast/route/route_test.go
|
||||
- [x] **T005** Create parameter binding test in pkg/ast/route/route_test.go
|
||||
- [x] **T006** Create error handling test in pkg/ast/route/route_test.go
|
||||
|
||||
### Compatibility Tests
|
||||
- [ ] **T007** Test backward compatibility with existing annotations
|
||||
- [ ] **T008** Test cmd/gen_route.go integration
|
||||
|
||||
## Phase 3.3: Core Implementation (ONLY after tests are failing)
|
||||
|
||||
### Route Logic Refactoring
|
||||
- [ ] **T009** Refactor route.go parsing logic for better readability
|
||||
- [ ] **T010** Refactor builder.go for clearer separation of concerns
|
||||
- [ ] **T011** Improve error handling and diagnostics
|
||||
- [ ] **T012** Optimize render.go coordination logic
|
||||
|
||||
### Template and Rendering
|
||||
- [ ] **T013** Update router.go.tpl template if needed
|
||||
- [ ] **T014** Improve renderer.go wrapper functionality
|
||||
|
||||
## Phase 3.4: Integration
|
||||
|
||||
### CLI Integration
|
||||
- [ ] **T015** Verify cmd/gen_route.go works with refactored code
|
||||
- [ ] **T016** Test all existing functionality still works
|
||||
- [ ] **T017** Ensure performance targets are met (< 2s parsing)
|
||||
|
||||
## Phase 3.5: Polish
|
||||
|
||||
### Final Validation
|
||||
- [ ] **T018** Run comprehensive tests
|
||||
- [ ] **T019** Verify no breaking changes
|
||||
- [ ] **T020** Update documentation if needed
|
||||
|
||||
## Dependencies
|
||||
- Tests (T004-T008) before implementation (T009-T014)
|
||||
- Implementation before integration (T015-T017)
|
||||
- Integration before polish (T018-T020)
|
||||
|
||||
## Parallel Example
|
||||
|
||||
```
|
||||
# Launch basic tests together (T004-T006):
|
||||
Task: "Create basic route parsing test in pkg/ast/route/route_test.go"
|
||||
Task: "Create parameter binding test in pkg/ast/route/route_test.go"
|
||||
Task: "Create error handling test in pkg/ast/route/route_test.go"
|
||||
|
||||
# Launch compatibility tests together (T007-T008):
|
||||
Task: "Test backward compatibility with existing annotations"
|
||||
Task: "Test cmd/gen_route.go integration"
|
||||
|
||||
# Launch refactoring tasks together (T009-T012):
|
||||
Task: "Refactor route.go parsing logic for better readability"
|
||||
Task: "Refactor builder.go for clearer separation of concerns"
|
||||
Task: "Improve error handling and diagnostics"
|
||||
Task: "Optimize render.go coordination logic"
|
||||
```
|
||||
|
||||
## Notes
|
||||
- [P] tasks = different components/files, no dependencies
|
||||
- Verify tests fail before implementing (TDD)
|
||||
- Focus on minimal refactoring for better readability
|
||||
- Keep business files flat - no complex directory structures
|
||||
- Ensure backward compatibility with existing @Router and @Bind annotations
|
||||
- Ensure cmd/gen_route.go interface remains unchanged
|
||||
- Follow KISS principle - minimal changes for maximum clarity
|
||||
|
||||
## Task Generation Rules Compliance
|
||||
|
||||
### SOLID Compliance
|
||||
- ✅ Single Responsibility: Each task focuses on one specific component
|
||||
- ✅ Open/Closed: Interface-based design allows extension without modification
|
||||
- ✅ Interface Segregation: Focused interfaces for different components
|
||||
- ✅ Dependency Inversion: Components depend on interfaces, not implementations
|
||||
|
||||
### KISS Compliance
|
||||
- ✅ Simple, direct task descriptions
|
||||
- ✅ Clear file organization and naming
|
||||
- ✅ Minimal dependencies between tasks
|
||||
|
||||
### YAGNI Compliance
|
||||
- ✅ Only essential tasks for refactoring goals
|
||||
- ✅ No speculative functionality
|
||||
- ✅ Focus on MVP refactoring first
|
||||
|
||||
### DRY Compliance
|
||||
- ✅ Consolidated similar operations
|
||||
- ✅ Reused patterns from provider module
|
||||
- ✅ No duplicate task definitions
|
||||
|
||||
## Validation Checklist
|
||||
- [x] All contracts have corresponding tests (T004-T005)
|
||||
- [x] All entities have model tasks (T010-T012)
|
||||
- [x] All tests come before implementation
|
||||
- [x] Parallel tasks truly independent
|
||||
- [x] Each task specifies exact file path
|
||||
- [x] No task modifies same file as another [P] task
|
||||
- [x] Backward compatibility maintained throughout
|
||||
- [x] cmd/gen_route.go integration included
|
||||
- [x] Performance considerations addressed (< 2s parsing)
|
||||
Reference in New Issue
Block a user