Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 56 additions & 18 deletions internal/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,85 @@ import (
"strings"
)

// GenerationTask describes one template-to-file emission step.
type GenerationTask struct {
TemplateFile string
OutputFile string
}

// GenerationPlan captures the ordered set of files that will be generated.
type GenerationPlan struct {
Tasks []GenerationTask
}

// Generate generates all code files from templates by scanning the template directory
func Generate(
templateLoader TemplateLoader,
templateData *TemplateData,
outputDir string,
) error {
// Ensure output directory exists
plan, err := BuildGenerationPlan(templateLoader, templateData, outputDir)
if err != nil {
return err
}

return executeGenerationPlan(templateLoader, templateData, plan)
}

// BuildGenerationPlan resolves the ordered set of template files to emit.
func BuildGenerationPlan(
templateLoader TemplateLoader,
templateData *TemplateData,
outputDir string,
) (*GenerationPlan, error) {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %v", err)
return nil, fmt.Errorf("failed to create output directory: %v", err)
}

// Get all template files
templateFiles, err := templateLoader.ListFiles()
if err != nil {
return fmt.Errorf("failed to list template files: %v", err)
return nil, fmt.Errorf("failed to list template files: %v", err)
}

// Generate files from templates
var generatedFiles []string

plan := &GenerationPlan{}
for _, templateFile := range templateFiles {
// Convert template name to output filename
// For example: "vips.go.tmpl" -> "vips.go"
outputFile := filepath.Join(outputDir, strings.TrimSuffix(filepath.Base(templateFile), ".tmpl"))

// Skip test templates if IncludeTest is false
if !templateData.IncludeTest && strings.HasSuffix(filepath.Base(templateFile), "_test.go.tmpl") {
log.Printf("Skipping test template: %s (use --include-test to generate)\n", filepath.Base(templateFile))
baseName := filepath.Base(templateFile)
if shouldSkipTemplate(baseName, templateData.IncludeTest) {
log.Printf("Skipping test template: %s (use --include-test to generate)\n", baseName)
continue
}

// Generate file from template
if err := templateLoader.GenerateFile(templateFile, outputFile, templateData); err != nil {
return fmt.Errorf("failed to generate %s: %v", outputFile, err)
plan.Tasks = append(plan.Tasks, GenerationTask{
TemplateFile: templateFile,
OutputFile: filepath.Join(outputDir, strings.TrimSuffix(baseName, ".tmpl")),
})
}

return plan, nil
}

func executeGenerationPlan(
templateLoader TemplateLoader,
templateData *TemplateData,
plan *GenerationPlan,
) error {
var generatedFiles []string

for _, task := range plan.Tasks {
if err := templateLoader.GenerateFile(task.TemplateFile, task.OutputFile, templateData); err != nil {
return fmt.Errorf("failed to generate %s: %v", task.OutputFile, err)
}
generatedFiles = append(generatedFiles, outputFile)
generatedFiles = append(generatedFiles, task.OutputFile)
}

log.Printf("\nSuccessfully generated files from templates: %d\n", len(generatedFiles))
for _, file := range generatedFiles {
log.Printf(" - %s\n", file)
}

return nil
}

func shouldSkipTemplate(templateFile string, includeTest bool) bool {
return !includeTest && strings.HasSuffix(templateFile, "_test.go.tmpl")
}
117 changes: 117 additions & 0 deletions internal/generator/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package generator

import (
"errors"
"path/filepath"
"reflect"
"strings"
"testing"
"text/template"
)

type stubTemplateLoader struct {
listFilesErr error
listFiles []string
generated []GenerationTask
generateErr error
generateAt int
}

func (s *stubTemplateLoader) LoadTemplate(name string) (*template.Template, error) {
return nil, nil
}

func (s *stubTemplateLoader) ListFiles() ([]string, error) {
if s.listFilesErr != nil {
return nil, s.listFilesErr
}
return append([]string(nil), s.listFiles...), nil
}

func (s *stubTemplateLoader) GenerateFile(templateName, outputFile string, data interface{}) error {
s.generated = append(s.generated, GenerationTask{TemplateFile: templateName, OutputFile: outputFile})
if s.generateErr != nil && len(s.generated) == s.generateAt {
return s.generateErr
}
return nil
}

func TestBuildGenerationPlanSkipsTestsWhenDisabled(t *testing.T) {
loader := &stubTemplateLoader{
listFiles: []string{"a.go.tmpl", "b_test.go.tmpl", "nested/c.go.tmpl"},
}
outputDir := t.TempDir()

plan, err := BuildGenerationPlan(loader, &TemplateData{}, outputDir)
if err != nil {
t.Fatalf("BuildGenerationPlan returned error: %v", err)
}

got := plan.Tasks
want := []GenerationTask{
{TemplateFile: "a.go.tmpl", OutputFile: filepath.Join(outputDir, "a.go")},
{TemplateFile: "nested/c.go.tmpl", OutputFile: filepath.Join(outputDir, "c.go")},
}

if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected plan tasks\n got: %#v\nwant: %#v", got, want)
}
}

func TestBuildGenerationPlanIncludesTestsWhenEnabled(t *testing.T) {
loader := &stubTemplateLoader{
listFiles: []string{"a.go.tmpl", "b_test.go.tmpl"},
}
outputDir := t.TempDir()

plan, err := BuildGenerationPlan(loader, &TemplateData{IncludeTest: true}, outputDir)
if err != nil {
t.Fatalf("BuildGenerationPlan returned error: %v", err)
}

got := []string{plan.Tasks[0].OutputFile, plan.Tasks[1].OutputFile}
want := []string{filepath.Join(outputDir, "a.go"), filepath.Join(outputDir, "b_test.go")}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected output files\n got: %#v\nwant: %#v", got, want)
}
}

func TestGeneratePreservesExecutionOrder(t *testing.T) {
loader := &stubTemplateLoader{
listFiles: []string{"z.go.tmpl", "a.go.tmpl", "b_test.go.tmpl"},
}

outputDir := t.TempDir()
if err := Generate(loader, &TemplateData{}, outputDir); err != nil {
t.Fatalf("Generate returned error: %v", err)
}

got := loader.generated
want := []GenerationTask{
{TemplateFile: "z.go.tmpl", OutputFile: filepath.Join(outputDir, "z.go")},
{TemplateFile: "a.go.tmpl", OutputFile: filepath.Join(outputDir, "a.go")},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected generation order\n got: %#v\nwant: %#v", got, want)
}
}

func TestGenerateWrapsFileGenerationErrors(t *testing.T) {
loader := &stubTemplateLoader{
listFiles: []string{"a.go.tmpl", "b.go.tmpl"},
generateErr: errors.New("boom"),
generateAt: 2,
}
outputDir := t.TempDir()

err := Generate(loader, &TemplateData{}, outputDir)
if err == nil {
t.Fatal("Generate returned nil error")
}
if !strings.Contains(err.Error(), filepath.Join(outputDir, "b.go")) {
t.Fatalf("expected error to mention output file, got: %v", err)
}
if !strings.Contains(err.Error(), "boom") {
t.Fatalf("expected wrapped cause, got: %v", err)
}
}
Loading
Loading