feat: 重构 pkg/ast/provider 模块,优化代码组织逻辑和功能实现

## 主要改进

### 架构重构
- 将单体 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.
This commit is contained in:
Rogee
2025-09-19 18:58:30 +08:00
parent 8c65c6a854
commit e1f83ae469
45 changed files with 8643 additions and 313 deletions

View File

@@ -11,10 +11,10 @@ import (
)
func CommandBuf(root *cobra.Command) {
cmd := &cobra.Command{
Use: "buf",
Short: "run buf commands",
Long: `在指定目录执行 buf generate。若本机未安装 buf将自动 go install github.com/bufbuild/buf/cmd/buf@v1.48.0。
cmd := &cobra.Command{
Use: "buf",
Short: "run buf commands",
Long: `在指定目录执行 buf generate。若本机未安装 buf将自动 go install github.com/bufbuild/buf/cmd/buf@v1.48.0。
Flags:
- --dir 执行目录(默认 .
@@ -23,8 +23,8 @@ Flags:
说明:
- 运行前会检查 buf.yaml 是否存在,如不存在会给出提示但仍尝试执行
- 成功后输出生成结果日志`,
RunE: commandBufE,
}
RunE: commandBufE,
}
cmd.Flags().String("dir", ".", "Directory to run buf from")
cmd.Flags().Bool("dry-run", false, "Preview buf command without executing")

View File

@@ -22,17 +22,17 @@ Flags:
说明:
- 正常格式化等价于gofumpt -l -extra -w <path>
- 检查模式等价于gofumpt -l -extra <path>`,
RunE: commandFmtE,
RunE: commandFmtE,
}
cmd.Flags().Bool("check", false, "Check formatting without writing changes")
cmd.Flags().String("path", ".", "Path to format (default .)")
cmd.Flags().Bool("check", false, "Check formatting without writing changes")
cmd.Flags().String("path", ".", "Path to format (default .)")
root.AddCommand(cmd)
}
func commandFmtE(cmd *cobra.Command, args []string) error {
log.Info("开始格式化代码")
log.Info("开始格式化代码")
if _, err := exec.LookPath("gofumpt"); err != nil {
log.Info("gofumpt 不存在,正在安装...")
installCmd := exec.Command("go", "install", "mvdan.cc/gofumpt@latest")
@@ -46,31 +46,31 @@ func commandFmtE(cmd *cobra.Command, args []string) error {
}
}
check, _ := cmd.Flags().GetBool("check")
path, _ := cmd.Flags().GetString("path")
check, _ := cmd.Flags().GetBool("check")
path, _ := cmd.Flags().GetString("path")
if check {
log.Info("运行 gofumpt 检查模式...")
out, err := exec.Command("gofumpt", "-l", "-extra", path).CombinedOutput()
if err != nil {
return fmt.Errorf("运行 gofumpt 失败: %v", err)
}
if len(out) > 0 {
fmt.Fprintln(os.Stdout, string(out))
return fmt.Errorf("发现未格式化文件,请运行: gofumpt -l -extra -w %s", path)
}
log.Info("代码格式良好")
return nil
}
if check {
log.Info("运行 gofumpt 检查模式...")
out, err := exec.Command("gofumpt", "-l", "-extra", path).CombinedOutput()
if err != nil {
return fmt.Errorf("运行 gofumpt 失败: %v", err)
}
if len(out) > 0 {
fmt.Fprintln(os.Stdout, string(out))
return fmt.Errorf("发现未格式化文件,请运行: gofumpt -l -extra -w %s", path)
}
log.Info("代码格式良好")
return nil
}
log.Info("运行 gofumpt...")
gofumptCmd := exec.Command("gofumpt", "-l", "-extra", "-w", path)
gofumptCmd.Stdout = os.Stdout
gofumptCmd.Stderr = os.Stderr
if err := gofumptCmd.Run(); err != nil {
return fmt.Errorf("运行 gofumpt 失败: %v", err)
}
log.Info("运行 gofumpt...")
gofumptCmd := exec.Command("gofumpt", "-l", "-extra", "-w", path)
gofumptCmd.Stdout = os.Stdout
gofumptCmd.Stderr = os.Stderr
if err := gofumptCmd.Run(); err != nil {
return fmt.Errorf("运行 gofumpt 失败: %v", err)
}
log.Info("格式化代码完成")
return nil
log.Info("格式化代码完成")
return nil
}

View File

@@ -3,18 +3,18 @@ package cmd
import "github.com/spf13/cobra"
func CommandGen(root *cobra.Command) {
cmd := &cobra.Command{
Use: "gen",
Short: "Generate code",
Long: `代码生成命令组:包含 route、provider、model、enum、service 等。
cmd := &cobra.Command{
Use: "gen",
Short: "Generate code",
Long: `代码生成命令组:包含 route、provider、model、enum、service 等。
持久化参数:
- -c, --config 数据库配置文件(默认 config.toml供 gen model 使用
说明:
- 子命令执行完成后会自动运行 atomctl fmt 进行格式化`,
PersistentPostRunE: commandFmtE,
}
PersistentPostRunE: commandFmtE,
}
cmd.PersistentFlags().StringP("config", "c", "config.toml", "database config file")
cmds := []func(*cobra.Command){

View File

@@ -1,14 +1,14 @@
package cmd
import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
apg "go.ipao.vip/atomctl/v2/pkg/postgres"
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
"go.ipao.vip/gen"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
apg "go.ipao.vip/atomctl/v2/pkg/postgres"
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
"go.ipao.vip/gen"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func CommandGenModel(root *cobra.Command) {
@@ -29,38 +29,38 @@ func CommandGenModel(root *cobra.Command) {
示例:
atomctl gen -c config.toml model`,
RunE: commandGenModelE,
RunE: commandGenModelE,
}
root.AddCommand(cmd)
}
func commandGenModelE(cmd *cobra.Command, args []string) error {
if err := gomod.Parse("go.mod"); err != nil {
return errors.Wrap(err, "parse go.mod")
}
if err := gomod.Parse("go.mod"); err != nil {
return errors.Wrap(err, "parse go.mod")
}
cfgFile := cmd.Flag("config").Value.String()
if cfgFile == "" {
cfgFile = "config.toml"
}
cfgFile := cmd.Flag("config").Value.String()
if cfgFile == "" {
cfgFile = "config.toml"
}
sqlDB, conf, err := apg.GetDB(cfgFile)
if err != nil {
return errors.Wrap(err, "load database config")
}
defer sqlDB.Close()
sqlDB, conf, err := apg.GetDB(cfgFile)
if err != nil {
return errors.Wrap(err, "load database config")
}
defer sqlDB.Close()
dsn := conf.DSN()
log.Infof("parsed DSN: %s (schema=%s)", dsn, conf.Schema)
dsn := conf.DSN()
log.Infof("parsed DSN: %s (schema=%s)", dsn, conf.Schema)
db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}))
if err != nil {
return errors.Wrapf(err, "open database with dsn: %s", dsn)
}
db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}))
if err != nil {
return errors.Wrapf(err, "open database with dsn: %s", dsn)
}
// 默认同包同目录生成到 ./database
gen.GenerateWithDefault(db, "./database/.transform.yaml")
return nil
return nil
}

View File

@@ -15,10 +15,10 @@ import (
)
func CommandGenRoute(root *cobra.Command) {
cmd := &cobra.Command{
Use: "route",
Short: "generate routes",
Long: `扫描项目控制器,解析注释生成 routes.gen.go。
cmd := &cobra.Command{
Use: "route",
Short: "generate routes",
Long: `扫描项目控制器,解析注释生成 routes.gen.go。
用法与规则:
- 扫描根目录通过 --path 指定(默认 CWD会在 <path>/app/http 下递归搜索。
@@ -37,9 +37,9 @@ func CommandGenRoute(root *cobra.Command) {
- local任意类型上下文本地值
说明:生成完成后会自动运行 gen provider 以补全依赖注入。`,
RunE: commandGenRouteE,
PostRunE: commandGenProviderE,
}
RunE: commandGenRouteE,
PostRunE: commandGenProviderE,
}
cmd.Flags().String("path", ".", "Base path to scan (defaults to CWD)")

View File

@@ -14,8 +14,8 @@ import (
func CommandGenService(root *cobra.Command) {
cmd := &cobra.Command{
Use: "service",
Short: "generate services",
Use: "service",
Short: "generate services",
Long: `扫描 --path 指定目录(默认 ./app/services下的 Go 文件,汇总服务名并渲染生成 services.gen.go。
规则:

View File

@@ -5,10 +5,10 @@ import (
)
func CommandInit(root *cobra.Command) {
cmd := &cobra.Command{
Use: "new [project|module]",
Short: "new project/module",
Long: `脚手架命令组:创建项目与常用组件模板。
cmd := &cobra.Command{
Use: "new [project|module]",
Short: "new project/module",
Long: `脚手架命令组:创建项目与常用组件模板。
持久化参数(所有子命令通用):
- --force, -f 覆盖已存在文件/目录
@@ -16,19 +16,19 @@ func CommandInit(root *cobra.Command) {
- --dir 指定输出基目录(默认 .
子命令project、provider、event、jobmodule 已弃用)`,
}
}
cmd.PersistentFlags().BoolP("force", "f", false, "Force overwrite existing files or directories")
cmd.PersistentFlags().Bool("dry-run", false, "Preview actions without writing files")
cmd.PersistentFlags().String("dir", ".", "Base directory for outputs")
cmd.PersistentFlags().BoolP("force", "f", false, "Force overwrite existing files or directories")
cmd.PersistentFlags().Bool("dry-run", false, "Preview actions without writing files")
cmd.PersistentFlags().String("dir", ".", "Base directory for outputs")
cmds := []func(*cobra.Command){
CommandNewProject,
// deprecate CommandNewModule,
CommandNewProvider,
CommandNewEvent,
CommandNewJob,
}
cmds := []func(*cobra.Command){
CommandNewProject,
// deprecate CommandNewModule,
CommandNewProvider,
CommandNewEvent,
CommandNewJob,
}
for _, c := range cmds {
c(cmd)

View File

@@ -31,8 +31,8 @@ func CommandNewEvent(root *cobra.Command) {
示例:
atomctl new event UserCreated
atomctl new event UserCreated --only=publisher`,
Args: cobra.ExactArgs(1),
RunE: commandNewEventE,
Args: cobra.ExactArgs(1),
RunE: commandNewEventE,
}
cmd.Flags().String("only", "", "仅生成: publisher 或 subscriber")

View File

@@ -42,7 +42,7 @@ func CommandNewProject(root *cobra.Command) {
atomctl new project github.com/acme/demo
atomctl new -f --dir ./playground project github.com/acme/demo
atomctl new project # 在已有 go.mod 的项目中就地初始化`,
RunE: commandNewProjectE,
RunE: commandNewProjectE,
}
root.AddCommand(cmd)

View File

@@ -1,26 +1,26 @@
package cmd
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"github.com/iancoleman/strcase"
"github.com/spf13/cobra"
"go.ipao.vip/atomctl/v2/templates"
"github.com/iancoleman/strcase"
"github.com/spf13/cobra"
"go.ipao.vip/atomctl/v2/templates"
)
// CommandNewProvider 注册 new_provider 命令
func CommandNewProvider(root *cobra.Command) {
cmd := &cobra.Command{
Use: "provider",
Short: "创建新的 provider",
Long: `在 providers/<name> 目录下渲染创建 Provider 模板。
cmd := &cobra.Command{
Use: "provider",
Short: "创建新的 provider",
Long: `在 providers/<name> 目录下渲染创建 Provider 模板。
行为:
- 当 name 与内置预置目录同名时,渲染该目录;否则回退渲染 providers/default
@@ -33,35 +33,35 @@ func CommandNewProvider(root *cobra.Command) {
示例:
atomctl new provider email
atomctl new --dry-run --dir ./demo provider cache`,
Args: cobra.MaximumNArgs(1),
RunE: commandNewProviderE,
}
Args: cobra.MaximumNArgs(1),
RunE: commandNewProviderE,
}
root.AddCommand(cmd)
}
func commandNewProviderE(cmd *cobra.Command, args []string) error {
// no-arg: list available preset providers
if len(args) == 0 {
entries, err := templates.Providers.ReadDir("providers")
if err != nil {
return err
}
var names []string
for _, e := range entries {
if e.IsDir() {
names = append(names, e.Name())
}
}
sort.Strings(names)
fmt.Println("可用预置 providers:")
for _, n := range names {
fmt.Printf(" - %s\n", n)
}
return nil
}
// no-arg: list available preset providers
if len(args) == 0 {
entries, err := templates.Providers.ReadDir("providers")
if err != nil {
return err
}
var names []string
for _, e := range entries {
if e.IsDir() {
names = append(names, e.Name())
}
}
sort.Strings(names)
fmt.Println("可用预置 providers:")
for _, n := range names {
fmt.Printf(" - %s\n", n)
}
return nil
}
providerName := args[0]
providerName := args[0]
// shared flags
dryRun, _ := cmd.Flags().GetBool("dry-run")
baseDir, _ := cmd.Flags().GetString("dir")
@@ -80,38 +80,38 @@ func commandNewProviderE(cmd *cobra.Command, args []string) error {
}
}
// choose template source: providers/<name> or providers/default
srcDir := filepath.Join("providers", providerName)
if _, err := templates.Providers.ReadDir(srcDir); err != nil {
srcDir = filepath.Join("providers", "default")
}
// choose template source: providers/<name> or providers/default
srcDir := filepath.Join("providers", providerName)
if _, err := templates.Providers.ReadDir(srcDir); err != nil {
srcDir = filepath.Join("providers", "default")
}
err := fs.WalkDir(templates.Providers, srcDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
err := fs.WalkDir(templates.Providers, srcDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
relPath, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
relPath, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
destPath := filepath.Join(targetPath, strings.TrimSuffix(relPath, ".tpl"))
if dryRun {
fmt.Printf("[dry-run] mkdir -p %s\n", filepath.Dir(destPath))
} else {
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
}
destPath := filepath.Join(targetPath, strings.TrimSuffix(relPath, ".tpl"))
if dryRun {
fmt.Printf("[dry-run] mkdir -p %s\n", filepath.Dir(destPath))
} else {
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
}
tmpl, err := template.ParseFS(templates.Providers, path)
if err != nil {
return err
}
tmpl, err := template.ParseFS(templates.Providers, path)
if err != nil {
return err
}
if dryRun {
fmt.Printf("[dry-run] render > %s\n", destPath)

View File

@@ -15,21 +15,21 @@ func CommandSwagFmt(root *cobra.Command) {
参数:
- --dir 扫描目录(默认 ./app/http
- --main 主入口文件(默认 main.go`,
RunE: commandSwagFmtE,
RunE: commandSwagFmtE,
}
cmd.Flags().String("dir", "./app/http", "SearchDir for swag format")
cmd.Flags().String("main", "main.go", "MainFile for swag format")
cmd.Flags().String("dir", "./app/http", "SearchDir for swag format")
cmd.Flags().String("main", "main.go", "MainFile for swag format")
root.AddCommand(cmd)
}
func commandSwagFmtE(cmd *cobra.Command, args []string) error {
dir := cmd.Flag("dir").Value.String()
main := cmd.Flag("main").Value.String()
return format.New().Build(&format.Config{
SearchDir: dir,
Excludes: "",
MainFile: main,
})
dir := cmd.Flag("dir").Value.String()
main := cmd.Flag("main").Value.String()
return format.New().Build(&format.Config{
SearchDir: dir,
Excludes: "",
MainFile: main,
})
}

View File

@@ -23,41 +23,43 @@ func CommandSwagInit(root *cobra.Command) {
- --main 主入口文件(默认 main.go
说明:基于 rogeecn/swag 的 gen 构建器,支持模板分隔符定制、依赖解析等配置。`,
RunE: commandSwagInitE,
RunE: commandSwagInitE,
}
cmd.Flags().String("dir", ".", "SearchDir (project root)")
cmd.Flags().String("out", "docs", "Output dir for generated docs")
cmd.Flags().String("main", "main.go", "Main API file path")
cmd.Flags().String("dir", ".", "SearchDir (project root)")
cmd.Flags().String("out", "docs", "Output dir for generated docs")
cmd.Flags().String("main", "main.go", "Main API file path")
root.AddCommand(cmd)
}
func commandSwagInitE(cmd *cobra.Command, args []string) error {
root := cmd.Flag("dir").Value.String()
if root == "" {
var err error
root, err = os.Getwd()
if err != nil { return err }
}
root := cmd.Flag("dir").Value.String()
if root == "" {
var err error
root, err = os.Getwd()
if err != nil {
return err
}
}
leftDelim, rightDelim := "{{", "}}"
outDir := cmd.Flag("out").Value.String()
mainFile := cmd.Flag("main").Value.String()
outDir := cmd.Flag("out").Value.String()
mainFile := cmd.Flag("main").Value.String()
return gen.New().Build(&gen.Config{
SearchDir: root,
Excludes: "",
ParseExtension: "",
MainAPIFile: mainFile,
PropNamingStrategy: swag.CamelCase,
OutputDir: filepath.Join(root, outDir),
OutputTypes: []string{"go", "json", "yaml"},
ParseVendor: false,
ParseDependency: 0,
MarkdownFilesDir: "",
ParseInternal: false,
return gen.New().Build(&gen.Config{
SearchDir: root,
Excludes: "",
ParseExtension: "",
MainAPIFile: mainFile,
PropNamingStrategy: swag.CamelCase,
OutputDir: filepath.Join(root, outDir),
OutputTypes: []string{"go", "json", "yaml"},
ParseVendor: false,
ParseDependency: 0,
MarkdownFilesDir: "",
ParseInternal: false,
Strict: false,
GeneratedTime: false,
RequiredByDefault: false,