feat: Refactor AST generation routes workflow

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

View File

@@ -16,131 +16,237 @@ type RenderBuildOpts struct {
}
func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
rd := RenderData{
PackageName: opts.PackageName,
ProjectPackage: opts.ProjectPackage,
Imports: []string{},
Controllers: []string{},
Routes: make(map[string][]Router),
RouteGroups: []string{},
builder := &renderDataBuilder{
opts: opts,
data: RenderData{
PackageName: opts.PackageName,
ProjectPackage: opts.ProjectPackage,
Imports: []string{},
Controllers: []string{},
Routes: make(map[string][]Router),
RouteGroups: []string{},
},
imports: []string{},
controllers: []string{},
needsFieldImport: false,
}
imports := []string{}
controllers := []string{}
// Track if any param uses model lookup, which requires the field package.
needsFieldImport := false
return builder.build()
}
for _, route := range opts.Routes {
imports = append(imports, route.Imports...)
controllers = append(controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name))
type renderDataBuilder struct {
opts RenderBuildOpts
data RenderData
imports []string
controllers []string
needsFieldImport bool
}
for _, action := range route.Actions {
funcName := fmt.Sprintf("Func%d", len(action.Params))
if action.HasData {
funcName = "Data" + funcName
}
func (b *renderDataBuilder) build() (RenderData, error) {
b.processRoutes()
b.addRequiredImports()
b.dedupeAndSortImports()
b.dedupeAndSortControllers()
b.sortRouteGroups()
params := lo.FilterMap(action.Params, func(item ParamDefinition, _ int) (string, bool) {
tok := buildParamToken(item)
if tok == "" {
return "", false
}
if item.Model != "" {
needsFieldImport = true
}
return tok, true
})
return b.data, nil
}
rd.Routes[route.Name] = append(rd.Routes[route.Name], Router{
Method: strcase.ToCamel(action.Method),
Route: action.Route,
Controller: strcase.ToLowerCamel(route.Name),
Action: action.Name,
Func: funcName,
Params: params,
})
func (b *renderDataBuilder) processRoutes() {
for _, route := range b.opts.Routes {
b.collectRouteMetadata(route)
b.buildRouteActions(route)
}
}
func (b *renderDataBuilder) collectRouteMetadata(route RouteDefinition) {
b.imports = append(b.imports, route.Imports...)
b.controllers = append(b.controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name))
}
func (b *renderDataBuilder) buildRouteActions(route RouteDefinition) {
for _, action := range route.Actions {
router := b.buildRouter(route, action)
b.data.Routes[route.Name] = append(b.data.Routes[route.Name], router)
}
}
func (b *renderDataBuilder) buildRouter(route RouteDefinition, action ActionDefinition) Router {
funcName := b.generateFunctionName(action)
params := b.buildParameters(action.Params)
return Router{
Method: strcase.ToCamel(action.Method),
Route: action.Route,
Controller: strcase.ToLowerCamel(route.Name),
Action: action.Name,
Func: funcName,
Params: params,
}
}
func (b *renderDataBuilder) generateFunctionName(action ActionDefinition) string {
funcName := fmt.Sprintf("Func%d", len(action.Params))
if action.HasData {
funcName = "Data" + funcName
}
return funcName
}
func (b *renderDataBuilder) buildParameters(params []ParamDefinition) []string {
return lo.FilterMap(params, func(item ParamDefinition, _ int) (string, bool) {
token := buildParamToken(item)
if token == "" {
return "", false
}
}
if item.Model != "" {
b.needsFieldImport = true
}
return token, true
})
}
// Add field import if any model lookups are used
if needsFieldImport {
imports = append(imports, `field "go.ipao.vip/gen/field"`)
func (b *renderDataBuilder) addRequiredImports() {
if b.needsFieldImport {
b.imports = append(b.imports, `field "go.ipao.vip/gen/field"`)
}
}
// de-dup and sort imports/controllers for stable output
rd.Imports = lo.Uniq(imports)
sort.Strings(rd.Imports)
rd.Controllers = lo.Uniq(controllers)
sort.Strings(rd.Controllers)
func (b *renderDataBuilder) dedupeAndSortImports() {
b.data.Imports = lo.Uniq(b.imports)
sort.Strings(b.data.Imports)
}
// stable order for route groups and entries
for k := range rd.Routes {
rd.RouteGroups = append(rd.RouteGroups, k)
}
sort.Strings(rd.RouteGroups)
for _, k := range rd.RouteGroups {
items := rd.Routes[k]
sort.Slice(items, func(i, j int) bool {
if items[i].Method != items[j].Method {
return items[i].Method < items[j].Method
}
if items[i].Route != items[j].Route {
return items[i].Route < items[j].Route
}
return items[i].Action < items[j].Action
})
rd.Routes[k] = items
func (b *renderDataBuilder) dedupeAndSortControllers() {
b.data.Controllers = lo.Uniq(b.controllers)
sort.Strings(b.data.Controllers)
}
func (b *renderDataBuilder) sortRouteGroups() {
// Collect route groups
for k := range b.data.Routes {
b.data.RouteGroups = append(b.data.RouteGroups, k)
}
sort.Strings(b.data.RouteGroups)
return rd, nil
// Sort routes within each group
for _, groupName := range b.data.RouteGroups {
items := b.data.Routes[groupName]
b.sortRouteItems(items)
b.data.Routes[groupName] = items
}
}
func (b *renderDataBuilder) sortRouteItems(items []Router) {
sort.Slice(items, func(i, j int) bool {
if items[i].Method != items[j].Method {
return items[i].Method < items[j].Method
}
if items[i].Route != items[j].Route {
return items[i].Route < items[j].Route
}
return items[i].Action < items[j].Action
})
}
func buildParamToken(item ParamDefinition) string {
key := item.Name
key := item.getKey()
builder := &paramTokenBuilder{item: item, key: key}
return builder.build()
}
func (item ParamDefinition) getKey() string {
if item.Key != "" {
key = item.Key
return item.Key
}
return item.Name
}
switch item.Position {
type paramTokenBuilder struct {
item ParamDefinition
key string
}
func (b *paramTokenBuilder) build() string {
switch b.item.Position {
case PositionQuery:
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
return b.buildQueryParam()
case PositionHeader:
return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key)
return b.buildHeaderParam()
case PositionFile:
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key)
return b.buildFileParam()
case PositionCookie:
if item.Type == "string" {
return fmt.Sprintf(`CookieParam("%s")`, key)
}
return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key)
return b.buildCookieParam()
case PositionBody:
return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key)
return b.buildBodyParam()
case PositionPath:
// If a model field is specified, generate a model-lookup binder from path value.
if item.Model != "" {
field := "id"
fieldType := "int"
if strings.Contains(item.Model, ":") {
parts := strings.SplitN(item.Model, ":", 2)
if len(parts) == 2 {
field = parts[0]
fieldType = parts[1]
}
} else {
field = item.Model
}
tpl := `func(ctx fiber.Ctx) (*%s, error) {
v := fiber.Params[%s](ctx, "%s")
return %sQuery.WithContext(ctx).Where(field.NewUnsafeFieldRaw("%s = ?", v)).First()
}`
return fmt.Sprintf(tpl, item.Type, fieldType, key, item.Type, field)
}
return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
return b.buildPathParam()
case PositionLocal:
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
return b.buildLocalParam()
default:
return ""
}
return ""
}
func (b *paramTokenBuilder) buildQueryParam() string {
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(b.item.Type), b.item.Type, b.key)
}
func (b *paramTokenBuilder) buildHeaderParam() string {
return fmt.Sprintf(`Header[%s]("%s")`, b.item.Type, b.key)
}
func (b *paramTokenBuilder) buildFileParam() string {
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, b.key)
}
func (b *paramTokenBuilder) buildCookieParam() string {
if b.item.Type == "string" {
return fmt.Sprintf(`CookieParam("%s")`, b.key)
}
return fmt.Sprintf(`Cookie[%s]("%s")`, b.item.Type, b.key)
}
func (b *paramTokenBuilder) buildBodyParam() string {
return fmt.Sprintf(`Body[%s]("%s")`, b.item.Type, b.key)
}
func (b *paramTokenBuilder) buildPathParam() string {
if b.item.Model != "" {
return b.buildModelLookupPath()
}
return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(b.item.Type), b.item.Type, b.key)
}
func (b *paramTokenBuilder) buildModelLookupPath() string {
field, fieldType := b.parseModelField()
tpl := `func(ctx fiber.Ctx) (*%s, error) {
v := fiber.Params[%s](ctx, "%s")
return %sQuery.WithContext(ctx).Where(field.NewUnsafeFieldRaw("%s = ?", v)).First()
}`
return fmt.Sprintf(tpl, b.item.Type, fieldType, b.key, b.item.Type, field)
}
func (b *paramTokenBuilder) parseModelField() (string, string) {
field := "id"
fieldType := "int"
if strings.Contains(b.item.Model, ":") {
parts := strings.SplitN(b.item.Model, ":", 2)
if len(parts) == 2 {
field = parts[0]
fieldType = parts[1]
}
} else {
field = b.item.Model
}
return field, fieldType
}
func (b *paramTokenBuilder) buildLocalParam() string {
return fmt.Sprintf(`Local[%s]("%s")`, b.item.Type, b.key)
}
func scalarSuffix(t string) string {