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:
321
pkg/ast/provider/renderer.go
Normal file
321
pkg/ast/provider/renderer.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Renderer defines the interface for rendering provider code
|
||||
type Renderer interface {
|
||||
// Render renders providers to Go code
|
||||
Render(providers []Provider) ([]byte, error)
|
||||
|
||||
// RenderToFile renders providers to a file
|
||||
RenderToFile(providers []Provider, filePath string) error
|
||||
|
||||
// RenderToWriter renders providers to an io.Writer
|
||||
RenderToWriter(providers []Provider, writer io.Writer) error
|
||||
|
||||
// AddTemplate adds a custom template
|
||||
AddTemplate(name, content string) error
|
||||
|
||||
// RemoveTemplate removes a custom template
|
||||
RemoveTemplate(name string)
|
||||
|
||||
// GetTemplate returns a template by name
|
||||
GetTemplate(name string) (*template.Template, error)
|
||||
|
||||
// SetTemplateFuncs sets custom template functions
|
||||
SetTemplateFuncs(funcs template.FuncMap)
|
||||
}
|
||||
|
||||
// GoRenderer implements the Renderer interface for Go code generation
|
||||
type GoRenderer struct {
|
||||
templates map[string]*template.Template
|
||||
templateFuncs template.FuncMap
|
||||
outputConfig *OutputConfig
|
||||
customTemplates map[string]string
|
||||
}
|
||||
|
||||
// OutputConfig represents configuration for output generation
|
||||
type OutputConfig struct {
|
||||
Header string // Header comment for generated files
|
||||
PackageName string // Package name for generated code
|
||||
Imports map[string]string // Additional imports to include
|
||||
GeneratedTag string // Tag to mark generated code
|
||||
DateFormat string // Date format for timestamps
|
||||
TemplateDir string // Directory for custom templates
|
||||
IndentString string // String used for indentation
|
||||
LineEnding string // Line ending style ("\n" or "\r\n")
|
||||
}
|
||||
|
||||
// RenderContext represents the context for rendering
|
||||
type RenderContext struct {
|
||||
Providers []Provider
|
||||
Config *OutputConfig
|
||||
Timestamp time.Time
|
||||
PackageName string
|
||||
Imports map[string]string
|
||||
CustomData map[string]interface{}
|
||||
}
|
||||
|
||||
// NewGoRenderer creates a new GoRenderer with default configuration
|
||||
func NewGoRenderer() *GoRenderer {
|
||||
return &GoRenderer{
|
||||
templates: make(map[string]*template.Template),
|
||||
templateFuncs: defaultTemplateFuncs(),
|
||||
outputConfig: NewOutputConfig(),
|
||||
customTemplates: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// NewGoRendererWithConfig creates a new GoRenderer with custom configuration
|
||||
func NewGoRendererWithConfig(config *OutputConfig) *GoRenderer {
|
||||
if config == nil {
|
||||
config = NewOutputConfig()
|
||||
}
|
||||
|
||||
return &GoRenderer{
|
||||
templates: make(map[string]*template.Template),
|
||||
templateFuncs: defaultTemplateFuncs(),
|
||||
outputConfig: config,
|
||||
customTemplates: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// NewOutputConfig creates a new OutputConfig with default values
|
||||
func NewOutputConfig() *OutputConfig {
|
||||
return &OutputConfig{
|
||||
Header: "// Code generated by atomctl provider generator. DO NOT EDIT.",
|
||||
PackageName: "main",
|
||||
Imports: make(map[string]string),
|
||||
GeneratedTag: "go:generate",
|
||||
DateFormat: "2006-01-02 15:04:05",
|
||||
IndentString: "\t",
|
||||
LineEnding: "\n",
|
||||
}
|
||||
}
|
||||
|
||||
// Render implements Renderer.Render
|
||||
func (r *GoRenderer) Render(providers []Provider) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Create render context
|
||||
context := r.createRenderContext(providers)
|
||||
|
||||
// Render the main template
|
||||
tmpl, err := r.getOrCreateTemplate("provider", defaultProviderTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get provider template: %w", err)
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(&buf, context); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// RenderToFile implements Renderer.RenderToFile
|
||||
func (r *GoRenderer) RenderToFile(providers []Provider, filePath string) error {
|
||||
// Create directory if it doesn't exist
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Create file
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Render to file
|
||||
return r.RenderToWriter(providers, file)
|
||||
}
|
||||
|
||||
// RenderToWriter implements Renderer.RenderToWriter
|
||||
func (r *GoRenderer) RenderToWriter(providers []Provider, writer io.Writer) error {
|
||||
content, err := r.Render(providers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = writer.Write(content)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddTemplate implements Renderer.AddTemplate
|
||||
func (r *GoRenderer) AddTemplate(name, content string) error {
|
||||
tmpl, err := template.New(name).Funcs(r.templateFuncs).Parse(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse template %s: %w", name, err)
|
||||
}
|
||||
|
||||
r.templates[name] = tmpl
|
||||
r.customTemplates[name] = content
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTemplate implements Renderer.RemoveTemplate
|
||||
func (r *GoRenderer) RemoveTemplate(name string) {
|
||||
delete(r.templates, name)
|
||||
delete(r.customTemplates, name)
|
||||
}
|
||||
|
||||
// GetTemplate implements Renderer.GetTemplate
|
||||
func (r *GoRenderer) GetTemplate(name string) (*template.Template, error) {
|
||||
return r.getOrCreateTemplate(name, "")
|
||||
}
|
||||
|
||||
// SetTemplateFuncs implements Renderer.SetTemplateFuncs
|
||||
func (r *GoRenderer) SetTemplateFuncs(funcs template.FuncMap) {
|
||||
r.templateFuncs = funcs
|
||||
|
||||
// Re-compile all templates with new functions
|
||||
for name, content := range r.customTemplates {
|
||||
tmpl, err := template.New(name).Funcs(r.templateFuncs).Parse(content)
|
||||
if err != nil {
|
||||
continue // Keep the old template if compilation fails
|
||||
}
|
||||
r.templates[name] = tmpl
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
func (r *GoRenderer) createRenderContext(providers []Provider) *RenderContext {
|
||||
context := &RenderContext{
|
||||
Providers: providers,
|
||||
Config: r.outputConfig,
|
||||
Timestamp: time.Now(),
|
||||
PackageName: r.outputConfig.PackageName,
|
||||
Imports: make(map[string]string),
|
||||
CustomData: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// Collect all imports from providers
|
||||
for _, provider := range providers {
|
||||
for alias, path := range provider.Imports {
|
||||
context.Imports[path] = alias
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom imports
|
||||
for alias, path := range r.outputConfig.Imports {
|
||||
context.Imports[path] = alias
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
func (r *GoRenderer) getOrCreateTemplate(name, defaultContent string) (*template.Template, error) {
|
||||
if tmpl, exists := r.templates[name]; exists {
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
if defaultContent == "" {
|
||||
return nil, fmt.Errorf("template %s not found", name)
|
||||
}
|
||||
|
||||
tmpl, err := template.New(name).Funcs(r.templateFuncs).Parse(defaultContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse default template: %w", err)
|
||||
}
|
||||
|
||||
r.templates[name] = tmpl
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func defaultTemplateFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"toTitle": strings.Title,
|
||||
"trimPrefix": strings.TrimPrefix,
|
||||
"trimSuffix": strings.TrimSuffix,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"hasSuffix": strings.HasSuffix,
|
||||
"contains": strings.Contains,
|
||||
"replace": strings.Replace,
|
||||
"join": strings.Join,
|
||||
"split": strings.Split,
|
||||
"formatTime": formatTime,
|
||||
"quote": func(s string) string { return fmt.Sprintf("%q", s) },
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"mul": func(a, b int) int { return a * b },
|
||||
"div": func(a, b int) int { return a / b },
|
||||
"dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, fmt.Errorf("invalid dict call")
|
||||
}
|
||||
dict := make(map[string]interface{})
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("dict keys must be strings")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(t time.Time, format string) string {
|
||||
if format == "" {
|
||||
format = "2006-01-02 15:04:05"
|
||||
}
|
||||
return t.Format(format)
|
||||
}
|
||||
|
||||
// Default provider template
|
||||
const defaultProviderTemplate = `{{.Config.Header}}
|
||||
|
||||
// Generated at: {{.Timestamp.Format "2006-01-02 15:04:05"}}
|
||||
// Package: {{.PackageName}}
|
||||
|
||||
package {{.PackageName}}
|
||||
|
||||
import (
|
||||
{{range $path, $alias := .Imports}}"{{$path}}" {{if $alias}}"{{$alias}}"{{end}}
|
||||
{{end}}
|
||||
)
|
||||
|
||||
{{range $provider := .Providers}}
|
||||
// {{.StructName}} provider implementation
|
||||
// Mode: {{.Mode}}
|
||||
// Return Type: {{.ReturnType}}
|
||||
{{if .NeedPrepareFunc}}func (p *{{.StructName}}) Prepare() error {
|
||||
// Prepare logic for {{.StructName}}
|
||||
return nil
|
||||
}{{end}}
|
||||
|
||||
func New{{.StructName}}({{range $name, $param := .InjectParams}}{{$name}} {{if $param.Star}}*{{end}}{{$param.Type}}{{if ne $name (last $provider.InjectParams)}}, {{end}}{{end}}) {{.ReturnType}} {
|
||||
return &{{.StructName}}{
|
||||
{{range $name, $param := .InjectParams}}{{$name}}: {{$name}},
|
||||
{{end}}
|
||||
}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
`
|
||||
|
||||
// Utility functions for template rendering
|
||||
func last(m map[string]InjectParam) string {
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys[len(keys)-1]
|
||||
}
|
||||
Reference in New Issue
Block a user