template

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 29, 2025 License: MIT Imports: 10 Imported by: 0

README

Template System

The template package provides a rendering engine for generating project files from embedded templates. It uses Go's embed.FS to bundle template files into the binary and text/template for variable substitution.

Architecture

Components

The template system consists of three main parts:

  1. TemplateRenderer Interface - Defines operations for rendering templates (defined in internal/generator/interfaces)
  2. TemplateData - Schema for variables available to all templates
  3. Embedded Templates - .tmpl files bundled via embed.FS
Flow
embed.FS (templates) → Renderer → text/template → Rendered Output
                          ↓
                     TemplateData (variables)

Templates are stored in internal/templates/project/ and embedded at compile time. The Renderer reads templates from the embedded filesystem, executes them with TemplateData, and produces output files.

Variable Schema

All templates have access to TemplateData fields:

Field Type Description Example
ModuleName string Go module path github.com/user/myapp
ProjectName string Project name (last segment of module path) myapp
DBDriver string Database driver to use go-libsql, sqlite3, postgres
GoVersion string Go version for go.mod 1.25
Year int Current year for copyright notices 2025
Example Template Usage

In a template file (go.mod.tmpl):

module {{.ModuleName}}

go {{.GoVersion}}

With TemplateData:

data := TemplateData{
    ModuleName: "github.com/user/myapp",
    GoVersion:  "1.25",
}

Renders to:

module github.com/user/myapp

go 1.25

Template File Naming

Templates use the .tmpl extension and preserve directory structure:

Template Path Output Path
go.mod.tmpl go.mod
.gitignore.tmpl .gitignore
cmd/server/main.go.tmpl cmd/server/main.go
tracks.yaml.tmpl tracks.yaml

The .tmpl extension is stripped during rendering, and nested paths are preserved in the output.

Using the Renderer

Creating a Renderer
import (
    "github.com/anomalousventures/tracks/internal/generator/template"
    "github.com/anomalousventures/tracks/internal/templates"
)

renderer := template.NewRenderer(templates.FS)
Rendering to String
data := template.TemplateData{
    ModuleName:  "github.com/user/myapp",
    ProjectName: "myapp",
    GoVersion:   "1.25",
}

content, err := renderer.Render("go.mod.tmpl", data)
if err != nil {
    // handle error
}
Rendering to File

The RenderToFile method automatically creates parent directories:

outputPath := filepath.Join(projectDir, "go.mod")
err := renderer.RenderToFile("go.mod.tmpl", data, outputPath)
if err != nil {
    // handle error
}

For nested paths:

mainPath := filepath.Join(projectDir, "cmd", "server", "main.go")
err := renderer.RenderToFile("cmd/server/main.go.tmpl", data, mainPath)
// Creates cmd/ and cmd/server/ directories automatically
Validating Templates

Check if a template exists and has valid syntax:

err := renderer.Validate("go.mod.tmpl")
if err != nil {
    // Template is missing or has syntax errors
}

Adding New Templates

To add a new template to the system:

1. Create Template File

Create a .tmpl file in internal/templates/project/:

# Root-level template
internal/templates/project/myfile.txt.tmpl

# Nested template
internal/templates/project/config/settings.yaml.tmpl
2. Use Template Variables

Use {{.FieldName}} for variable substitution:

# settings.yaml.tmpl
app_name: {{.ProjectName}}
version: {{.GoVersion}}
database:
  driver: {{.DBDriver}}
3. Write Tests

Add tests in templates_test.go:

func TestMyFileTemplate(t *testing.T) {
    renderer := NewRenderer(templates.FS)

    data := TemplateData{
        ProjectName: "testapp",
        GoVersion:   "1.25",
        DBDriver:    "sqlite3",
    }

    result, err := renderer.Render("myfile.txt.tmpl", data)
    require.NoError(t, err)
    assert.Contains(t, result, "testapp")
}
4. Update Production Templates List

If the template is part of standard project generation, add it to productionTemplates in integration_test.go.

Cross-Platform Path Handling

The package uses two path packages for correct cross-platform behavior:

  • path - For embed.FS paths (always forward slashes)
  • filepath - For OS-specific file paths (uses OS separators)
Embed Paths (internal use)
embedPath := path.Join("project", templateName)  // Always uses /
content, err := fs.ReadFile(r.fs, embedPath)
File System Paths (user-facing)
outputPath := filepath.Join(projectDir, "cmd", "server", "main.go")
// Windows: project\cmd\server\main.go
// Unix:    project/cmd/server/main.go

This ensures templates work correctly on Windows, macOS, and Linux.

Error Handling

The package provides two error types:

TemplateError

Wraps errors from file I/O and rendering operations:

_, err := renderer.Render("nonexistent.tmpl", data)
if terr, ok := err.(*template.TemplateError); ok {
    fmt.Println("Template:", terr.Template)  // "nonexistent.tmpl"
    fmt.Println("Cause:", terr.Err)          // underlying error
}
ValidationError

Reports template syntax errors:

err := renderer.Validate("bad-syntax.tmpl")
if verr, ok := err.(*template.ValidationError); ok {
    fmt.Println("Template:", verr.Template)  // "bad-syntax.tmpl"
    fmt.Println("Message:", verr.Message)    // syntax error details
}

Both error types support error unwrapping:

errors.Unwrap(err)  // Get underlying error
errors.Is(err, fs.ErrNotExist)  // Check for specific errors

Testing Templates

Unit Tests

Test individual templates with various data combinations:

func TestTemplateWithEmptyData(t *testing.T) {
    renderer := NewRenderer(templates.FS)
    result, err := renderer.Render("template.tmpl", TemplateData{})
    require.NoError(t, err)
    assert.NotEmpty(t, result)
}
Integration Tests

Test that all templates work together:

func TestAllTemplatesRender(t *testing.T) {
    tmpDir := t.TempDir()
    renderer := NewRenderer(templates.FS)

    data := TemplateData{
        ModuleName:  "github.com/test/app",
        ProjectName: "app",
        GoVersion:   "1.25",
    }

    for _, tmpl := range productionTemplates {
        outputName := strings.TrimSuffix(tmpl, ".tmpl")
        outputPath := filepath.Join(tmpDir, outputName)

        err := renderer.RenderToFile(tmpl, data, outputPath)
        require.NoError(t, err)

        _, err = os.Stat(outputPath)
        require.NoError(t, err, "file should exist")
    }
}
Cross-Platform Tests

Verify templates work on all operating systems:

func TestCrossPlatform(t *testing.T) {
    tmpDir := t.TempDir()
    renderer := NewRenderer(templates.FS)

    // Test nested directory creation
    nestedPath := filepath.Join(tmpDir, "cmd", "server", "main.go")
    err := renderer.RenderToFile("cmd/server/main.go.tmpl", data, nestedPath)
    require.NoError(t, err)

    _, err = os.Stat(nestedPath)
    require.NoError(t, err)
}

Best Practices

Template Design
  1. Keep templates simple - Use variable substitution, avoid complex logic
  2. Make templates self-contained - Each template should be independently renderable
  3. Use descriptive variable names - {{.ProjectName}} not {{.Name}}
  4. Preserve formatting - Templates should produce properly formatted output
Variable Naming
  1. Use PascalCase - ModuleName, ProjectName
  2. Be specific - DBDriver not Driver
  3. Document in TemplateData - Add godoc comments for new fields
Testing
  1. Test with empty data - Ensure templates don't crash with zero values
  2. Test with different drivers - Verify DBDriver variations work
  3. Test cross-platform - Use filepath.Join in tests
  4. Test edge cases - Special characters, long names, etc.
File Organization
  1. Mirror output structure - Template path = output path + .tmpl
  2. Group related templates - Use subdirectories for logical grouping
  3. Use clear names - main.go.tmpl not m.tmpl

Extending the System

Adding New Variables

To add a new variable to TemplateData:

  1. Add field to struct in data.go (see example below)
  2. Update tests to include the new field
  3. Update documentation (this README and godoc)
  4. Use in templates: {{.Description}}

Example structure in data.go:

type TemplateData struct {
    // ... existing fields ...

    // Description is the project description for README
    Description string
}
Custom Renderer Implementations

The interfaces.TemplateRenderer interface allows custom implementations.

The interface is defined in internal/generator/interfaces following ADR-002 (Interface Placement in Consumer Packages):

// From internal/generator/interfaces/template_renderer.go
type TemplateRenderer interface {
    Render(name string, data any) (string, error)
    RenderToFile(templateName string, data any, outputPath string) error
    Validate(name string) error
}

Note: The data parameter uses any for flexibility, but implementations typically expect template.TemplateData. See the interface documentation for the design rationale.

Example custom renderer:

import "github.com/anomalousventures/tracks/internal/generator/interfaces"

type CachingRenderer struct {
    base  interfaces.TemplateRenderer
    cache map[string]string
}

func (r *CachingRenderer) Render(name string, data any) (string, error) {
    if cached, ok := r.cache[name]; ok {
        return cached, nil
    }

    result, err := r.base.Render(name, data)
    if err == nil {
        r.cache[name] = result
    }
    return result, err
}

Godoc

View full API documentation:

# Template implementation package
go doc github.com/anomalousventures/tracks/internal/generator/template
go doc github.com/anomalousventures/tracks/internal/generator/template.NewRenderer
go doc github.com/anomalousventures/tracks/internal/generator/template.TemplateData

# TemplateRenderer interface (defined in interfaces package per ADR-002)
go doc github.com/anomalousventures/tracks/internal/generator/interfaces.TemplateRenderer

Or visit:

Documentation

Overview

Package template provides a template rendering engine for generating project files.

The template system uses Go's embed.FS to bundle template files into the binary, eliminating external dependencies and ensuring templates are always available at runtime. Templates use Go's text/template syntax for variable substitution.

Architecture

The template system consists of three main components:

  1. Renderer interface - defines template rendering operations
  2. TemplateData struct - provides variables to templates
  3. Embedded templates - .tmpl files bundled via embed.FS

Templates are embedded from internal/templates/project/ and rendered using the Renderer interface with TemplateData for variable substitution.

Basic Usage

Create a renderer and render a template:

import "github.com/anomalousventures/tracks/internal/templates"

renderer := template.NewRenderer(templates.FS)
data := template.TemplateData{
    ModuleName:  "github.com/user/myapp",
    ProjectName: "myapp",
    GoVersion:   "1.25",
}

content, err := renderer.Render("go.mod.tmpl", data)
if err != nil {
    // handle error
}

Rendering to Files

Render directly to a file with automatic directory creation:

outputPath := filepath.Join(projectDir, "go.mod")
err := renderer.RenderToFile("go.mod.tmpl", data, outputPath)

Template Variables

All templates have access to TemplateData fields:

{{.ModuleName}}  - Go module path (e.g., github.com/user/myapp)
{{.ProjectName}} - Project name (e.g., myapp)
{{.DBDriver}}    - Database driver (go-libsql, sqlite3, postgres)
{{.GoVersion}}   - Go version (e.g., 1.25)
{{.Year}}        - Current year for copyright

Adding New Templates

To add a new template:

  1. Create a .tmpl file in internal/templates/project/
  2. Use {{.VariableName}} for variable substitution
  3. Add tests in templates_test.go
  4. Template path structure is preserved in output (cmd/server/main.go.tmpl → cmd/server/main.go)

Error Handling

The package provides two error types:

TemplateError     - wraps errors from file I/O and rendering
ValidationError   - reports template syntax errors

Both implement error unwrapping for error chain inspection.

Cross-Platform Support

The package uses filepath for OS-specific paths and path for embed.FS paths, ensuring correct behavior on Windows, macOS, and Linux.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewRenderer

func NewRenderer(fs embed.FS) interfaces.TemplateRenderer

NewRenderer creates a new template renderer that implements interfaces.TemplateRenderer. The provided embed.FS should contain template files in a "project" subdirectory.

Types

type TemplateData

type TemplateData struct {
	// ModuleName is the full Go module path for the generated project.
	// Example: "github.com/user/myapp"
	ModuleName string

	// ProjectName is the short name of the project, typically the last segment of the module path.
	// Example: "myapp"
	ProjectName string

	// DBDriver specifies the database driver to use in the generated project.
	// Valid values: "go-libsql", "sqlite3", "postgres"
	DBDriver string

	// GoVersion specifies the Go version to use in the generated project's go.mod file.
	// Example: "1.25"
	GoVersion string

	// Year is the current year, used for copyright notices in generated files.
	// Example: 2025
	Year int

	// EnvPrefix is the prefix for environment variables (used with Viper's SetEnvPrefix).
	// Default: "APP"
	// Example: "MYAPP" results in MYAPP_DATABASE_URL, MYAPP_SERVER_PORT, etc.
	EnvPrefix string

	// SecretKey is a cryptographically secure random key for session management.
	SecretKey string
}

TemplateData contains all variables available to templates during rendering. This struct defines the complete schema of data that can be used in template files.

type TemplateError

type TemplateError struct {
	// Template is the name of the template that caused the error
	Template string

	// Err is the underlying error
	Err error
}

TemplateError represents an error that occurred during template rendering. It wraps the underlying error with the template name for better context.

func (*TemplateError) Error

func (e *TemplateError) Error() string

Error implements the error interface for TemplateError. It returns a formatted error message that includes the template name and underlying error.

func (*TemplateError) Unwrap

func (e *TemplateError) Unwrap() error

Unwrap returns the underlying error for error chain unwrapping.

type ValidationError

type ValidationError struct {
	// Template is the name of the template being validated
	Template string

	// Field is the name of the field that failed validation (empty if not field-specific)
	Field string

	// Message describes the validation failure
	Message string
}

ValidationError represents an error that occurred during template validation. It includes the template name, the problematic field, and a descriptive message.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements the error interface for ValidationError. It returns a formatted error message with template name, field, and message.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL