## 主要改进 ### 架构重构 - 将单体 provider.go 拆分为多个专门的模块文件 - 实现了清晰的职责分离和模块化设计 - 遵循 SOLID 原则,提高代码可维护性 ### 新增功能 - **验证规则系统**: 实现了完整的 provider 验证框架 - **报告生成器**: 支持多种格式的验证报告 (JSON/HTML/Markdown/Text) - **解析器优化**: 重新设计了解析流程,提高性能和可扩展性 - **错误处理**: 增强了错误处理和诊断能力 ### 修复关键 Bug - 修复 @provider(job) 注解缺失 __job 注入参数的问题 - 统一了 job 和 cronjob 模式的处理逻辑 - 确保了 provider 生成的正确性和一致性 ### 代码质量提升 - 添加了完整的测试套件 - 引入了 golangci-lint 代码质量检查 - 优化了代码格式和结构 - 增加了详细的文档和规范 ### 文件结构优化 ``` pkg/ast/provider/ ├── types.go # 类型定义 ├── parser.go # 解析器实现 ├── validator.go # 验证规则 ├── report_generator.go # 报告生成 ├── renderer.go # 渲染器 ├── comment_parser.go # 注解解析 ├── modes.go # 模式定义 ├── errors.go # 错误处理 └── validator_test.go # 测试文件 ``` ### 兼容性 - 保持向后兼容性 - 支持现有的所有 provider 模式 - 优化了 API 设计和用户体验 This completes the implementation of T025-T029 tasks following TDD principles, including validation rules implementation and critical bug fixes.
155 lines
4.0 KiB
Go
155 lines
4.0 KiB
Go
package route
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/iancoleman/strcase"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type RenderBuildOpts struct {
|
|
PackageName string
|
|
ProjectPackage string
|
|
Routes []RouteDefinition
|
|
}
|
|
|
|
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{},
|
|
}
|
|
|
|
imports := []string{}
|
|
controllers := []string{}
|
|
// Track if any param uses model lookup, which requires the field package.
|
|
needsFieldImport := false
|
|
|
|
for _, route := range opts.Routes {
|
|
imports = append(imports, route.Imports...)
|
|
controllers = append(controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name))
|
|
|
|
for _, action := range route.Actions {
|
|
funcName := fmt.Sprintf("Func%d", len(action.Params))
|
|
if action.HasData {
|
|
funcName = "Data" + funcName
|
|
}
|
|
|
|
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
|
|
})
|
|
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Add field import if any model lookups are used
|
|
if needsFieldImport {
|
|
imports = append(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)
|
|
|
|
// 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
|
|
}
|
|
|
|
return rd, nil
|
|
}
|
|
|
|
func buildParamToken(item ParamDefinition) string {
|
|
key := item.Name
|
|
if item.Key != "" {
|
|
key = item.Key
|
|
}
|
|
|
|
switch item.Position {
|
|
case PositionQuery:
|
|
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
|
|
case PositionHeader:
|
|
return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key)
|
|
case PositionFile:
|
|
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key)
|
|
case PositionCookie:
|
|
if item.Type == "string" {
|
|
return fmt.Sprintf(`CookieParam("%s")`, key)
|
|
}
|
|
return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key)
|
|
case PositionBody:
|
|
return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key)
|
|
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)
|
|
case PositionLocal:
|
|
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func scalarSuffix(t string) string {
|
|
switch t {
|
|
case "string", "int", "int8", "int16", "int32", "int64",
|
|
"uint", "uint8", "uint16", "uint32", "uint64",
|
|
"float32", "float64", "bool":
|
|
return "Param"
|
|
}
|
|
return ""
|
|
}
|