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#, PHPpkg/reflectutil- Reflection utilities for safe type manipulationpkg/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:
-
Implement the function in the appropriate file:
// string_helpers.go func ToScreamingSnakeCase(s string) string { return strings.ToUpper(ToSnakeCase(s)) } -
Register in funcmap.go:
func BuildFuncMap() template.FuncMap { return template.FuncMap{ // ... existing functions "toScreamingSnakeCase": ToScreamingSnakeCase, } } -
Document in /docs/TEMPLATE_MODE.md
Error Handling
Custom error types in errors.go:
TemplateLoadError- Template file not found or unreadableTemplateParseError- Invalid template syntaxTemplateExecuteError- 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
- Template Parsing - Template is parsed once in
NewWriter(), not per execution - Reflection - Loop and safe access helpers use reflection; cached where possible
- Pre-computed Views -
FlatColumns,FlatTables, etc. computed once per data item - 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:
- Follow existing patterns (see similar functions)
- Add to appropriate category file
- Register in
funcmap.go - Update
/docs/TEMPLATE_MODE.md - Add tests
- Consider edge cases (nil, empty, invalid input)
See Also
- User Documentation - Complete template function reference
- Common Types Package - Centralized type mappings
- Reflect Utilities - Reflection helpers
- Models Package - Database schema models
- Go Template Docs - Official Go template documentation