Files
relspecgo/pkg/writers/template/README.md
T

8.4 KiB

Template Writer

Custom template-based writer for RelSpec that allows users to generate any output format using Go text templates.

Overview

The template writer provides a powerful and flexible way to transform database schemas into any desired format. It supports multiple execution modes and provides 80+ template functions for data transformation.

For complete user documentation, see: /docs/TEMPLATE_MODE.md

Architecture

Package Structure

pkg/writers/template/
├── README.md              # This file
├── writer.go              # Core writer with entrypoint mode logic
├── template_data.go       # Data structures passed to templates
├── funcmap.go             # Template function registry
├── string_helpers.go      # String manipulation functions
├── type_mappers.go        # SQL type conversion (delegates to commontypes)
├── filters.go             # Database object filtering
├── formatters.go          # JSON/YAML formatting utilities
├── loop_helpers.go        # Iteration and collection utilities
├── safe_access.go         # Safe map/array access functions
└── errors.go              # Custom error types

Dependencies

  • pkg/commontypes - Centralized type mappings for Go, TypeScript, Java, Python, Rust, C#, PHP
  • pkg/reflectutil - Reflection utilities for safe type manipulation
  • pkg/models - Database schema models

Writer Interface Implementation

Implements the standard writers.Writer interface:

type Writer interface {
    WriteDatabase(db *models.Database) error
    WriteSchema(schema *models.Schema) error
    WriteTable(table *models.Table) error
}

Execution Modes

The writer supports four execution modes via WriterOptions.Metadata["mode"]:

Mode Data Passed Output Use Case
database Full database Single file Reports, documentation
schema One schema at a time File per schema Schema-specific docs
script One script at a time File per script Script processing
table One table at a time File per table Model generation

Configuration

Writer is configured via WriterOptions.Metadata:

metadata := map[string]interface{}{
    "template_path":    "/path/to/template.tmpl",  // Required
    "mode":             "table",                    // Default: "database"
    "filename_pattern": "{{.Name}}.ts",            // Default: "{{.Name}}.txt"
}

Template Data Structure

Templates receive a TemplateData struct:

type TemplateData struct {
    // Primary data (one populated based on mode)
    Database *models.Database
    Schema   *models.Schema
    Script   *models.Script
    Table    *models.Table

    // Parent context
    ParentSchema   *models.Schema
    ParentDatabase *models.Database

    // Pre-computed views
    FlatColumns       []*models.FlatColumn
    FlatTables        []*models.FlatTable
    FlatConstraints   []*models.FlatConstraint
    FlatRelationships []*models.FlatRelationship
    Summary           *models.DatabaseSummary

    // User metadata
    Metadata map[string]interface{}
}

Function Categories

String Utilities (string_helpers.go)

Case conversion, pluralization, trimming, splitting, joining

Type Mappers (type_mappers.go)

SQL type conversion to 7+ programming languages (delegates to pkg/commontypes)

Filters (filters.go)

Database object filtering by pattern, type, constraints

Formatters (formatters.go)

JSON/YAML serialization, indentation, escaping, commenting

Loop Helpers (loop_helpers.go)

Enumeration, batching, reversing, sorting, grouping (uses pkg/reflectutil)

Safe Access (safe_access.go)

Safe map/array access without panics (uses pkg/reflectutil)

Adding New Functions

To add a new template function:

  1. Implement the function in the appropriate file:

    // string_helpers.go
    func ToScreamingSnakeCase(s string) string {
        return strings.ToUpper(ToSnakeCase(s))
    }
    
  2. Register in funcmap.go:

    func BuildFuncMap() template.FuncMap {
        return template.FuncMap{
            // ... existing functions
            "toScreamingSnakeCase": ToScreamingSnakeCase,
        }
    }
    
  3. Document in /docs/TEMPLATE_MODE.md

Error Handling

Custom error types in errors.go:

  • TemplateLoadError - Template file not found or unreadable
  • TemplateParseError - Invalid template syntax
  • TemplateExecuteError - Error during template execution

All errors wrap the underlying error for context.

Testing

# Run tests
go test ./pkg/writers/template/...

# Test with example data
cat > test.tmpl << 'EOF'
{{ range .Database.Schemas }}
Schema: {{ .Name }} ({{ len .Tables }} tables)
{{ end }}
EOF

relspec templ --from json --from-path schema.json --template test.tmpl

Multi-file Output

For multi-file modes, OutputPath is always the output directory, including when it does not yet exist or has no trailing slash. An empty path uses the current directory. Database mode continues to treat OutputPath as one explicit file.

The writer renders and validates all filenames and contents before writing. Names must be relative, may contain safe subdirectories, and cannot contain traversal, absolute/drive/UNC paths, backslashes, Windows device names, alternate streams, control characters, or trailing dots/spaces. Duplicate filenames (including case variants) and file/directory collisions fail before any artifact is replaced.

Output directories are created automatically. Existing symlink roots, parents, and files are rejected. Writes and renames use os.Root to retain containment if paths change during generation. Each file is staged exclusively, closed successfully, and renamed into place; existing permissions are preserved on replacement. Multi-file publication is per file, so an I/O error partway through finalization can still leave a mixed generation (the broader audit A16 staging/manifest work remains).

Filename Pattern Execution

The filename pattern is itself a template:

// Pattern: "{{.Schema}}/{{.Name | toCamelCase}}.ts"
// For table "user_profile" in schema "public"
// Generates: "public/userProfile.ts"

Available in pattern template:

  • .Name - Item name (schema/script/table)
  • .Schema - Schema name (for scripts/tables)
  • All template functions

Example Usage

As a Library

import (
    "git.warky.dev/wdevs/relspecgo/pkg/writers"
    "git.warky.dev/wdevs/relspecgo/pkg/writers/template"
)

// Create writer
metadata := map[string]interface{}{
    "template_path":    "model.tmpl",
    "mode":             "table",
    "filename_pattern": "{{.Name}}.go",
}

opts := &writers.WriterOptions{
    OutputPath:  "./models/",
    PackageName: "models",
    Metadata:    metadata,
}

writer, err := template.NewWriter(opts)
if err != nil {
    // Handle error
}

// Write database
err = writer.WriteDatabase(db)

Via CLI

relspec templ \
    --from pgsql \
    --from-conn "postgres://localhost/mydb" \
    --template model.tmpl \
    --mode table \
    --output ./models/ \
    --filename-pattern "{{.Name | toPascalCase}}.go"

Performance Considerations

  1. Template Parsing - Template is parsed once in NewWriter(), not per execution
  2. Reflection - Loop and safe access helpers use reflection; cached where possible
  3. Pre-computed Views - FlatColumns, FlatTables, etc. computed once per data item
  4. File I/O - Multi-file mode creates directories as needed

Future Enhancements

Potential improvements:

  • Template caching for filename patterns
  • Parallel template execution for multi-file mode
  • Template function plugins
  • Custom function injection via metadata
  • Template includes/partials support
  • Dry-run mode to preview filenames
  • Progress reporting for large schemas

Contributing

When adding new features:

  1. Follow existing patterns (see similar functions)
  2. Add to appropriate category file
  3. Register in funcmap.go
  4. Update /docs/TEMPLATE_MODE.md
  5. Add tests
  6. Consider edge cases (nil, empty, invalid input)

See Also