template

package
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Jan 3, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

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, the writer:

  1. Iterates through items (schemas/scripts/tables)
  2. Creates TemplateData for each item
  3. Executes template with item data
  4. Generates filename using filename_pattern template
  5. Writes output to generated filename

Output directory is created automatically if it doesn't exist.

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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Batch

func Batch(slice interface{}, size int) [][]interface{}

Batch splits a slice into chunks of specified size Usage: {{ range batch .Columns 3 }}...{{ end }}

func BuildFuncMap

func BuildFuncMap() template.FuncMap

BuildFuncMap creates a template.FuncMap with all available helper functions

func Chunk

func Chunk(slice interface{}, size int) [][]interface{}

Chunk is an alias for Batch

func Comment

func Comment(s string, style string) string

Comment adds comment prefix to a string Supports: "//" (Go, C++, etc.), "#" (Python, shell), "--" (SQL), "/* */" (block) Usage: {{ .Table.Description | comment "//" }}

func Concat

func Concat(slices ...interface{}) []interface{}

Concat concatenates multiple slices Usage: {{ $all := concat .Schema1.Tables .Schema2.Tables }}

func CountIf

func CountIf(slice interface{}, matches func(interface{}) bool) int

CountIf counts items matching a condition Note: Since templates can't pass functions, this is limited Usage in code (not directly in templates)

func Escape

func Escape(s string) string

Escape escapes special characters in a string for use in code Usage: {{ .Column.Default | escape }}

func EscapeQuotes

func EscapeQuotes(s string) string

EscapeQuotes escapes only quote characters Usage: {{ .Column.Comment | escapeQuotes }}

func FilterCheckConstraints

func FilterCheckConstraints(constraints map[string]*models.Constraint) []*models.Constraint

FilterCheckConstraints returns only check constraints Usage: {{ $checks := filterCheckConstraints .Table.Constraints }}

func FilterColumns

func FilterColumns(columns map[string]*models.Column, pattern string) []*models.Column

FilterColumns filters columns from a map using a pattern Usage: {{ $filtered := filterColumns .Table.Columns "*_id" }}

func FilterColumnsByType

func FilterColumnsByType(columns map[string]*models.Column, sqlType string) []*models.Column

FilterColumnsByType filters columns by SQL type Usage: {{ $stringCols := filterColumnsByType .Table.Columns "varchar" }}

func FilterForeignKeys

func FilterForeignKeys(constraints map[string]*models.Constraint) []*models.Constraint

FilterForeignKeys returns only foreign key constraints Usage: {{ $fks := filterForeignKeys .Table.Constraints }}

func FilterNotNull

func FilterNotNull(columns map[string]*models.Column) []*models.Column

FilterNotNull returns only non-nullable columns Usage: {{ $required := filterNotNull .Table.Columns }}

func FilterNullable

func FilterNullable(columns map[string]*models.Column) []*models.Column

FilterNullable returns only nullable columns Usage: {{ $nullables := filterNullable .Table.Columns }}

func FilterPrimaryKeys

func FilterPrimaryKeys(columns map[string]*models.Column) []*models.Column

FilterPrimaryKeys returns only columns that are primary keys Usage: {{ $pks := filterPrimaryKeys .Table.Columns }}

func FilterTables

func FilterTables(tables []*models.Table, pattern string) []*models.Table

FilterTables filters tables using a predicate function Usage: {{ $filtered := filterTables .Schema.Tables (func $t) { return hasPrefix $t.Name "user_" } }} Note: Template functions can't pass Go funcs, so this is primarily for internal use

func FilterTablesByPattern

func FilterTablesByPattern(tables []*models.Table, pattern string) []*models.Table

FilterTablesByPattern filters tables by name pattern (glob-style) Usage: {{ $userTables := filterTablesByPattern .Schema.Tables "user_*" }}

func FilterUniqueConstraints

func FilterUniqueConstraints(constraints map[string]*models.Constraint) []*models.Constraint

FilterUniqueConstraints returns only unique constraints Usage: {{ $uniques := filterUniqueConstraints .Table.Constraints }}

func First

func First(slice interface{}, n int) []interface{}

First returns the first N items from a slice Usage: {{ range first .Tables 5 }}...{{ end }}

func Get

func Get(m interface{}, key interface{}) interface{}

Get safely gets a value from a map by key Usage: {{ get .Metadata "key" }}

func GetOr

func GetOr(m interface{}, key interface{}, defaultValue interface{}) interface{}

GetOr safely gets a value from a map with a default fallback Usage: {{ getOr .Metadata "key" "default" }}

func GetPath

func GetPath(m interface{}, path string) interface{}

GetPath safely gets a nested value using dot notation Usage: {{ getPath .Config "database.connection.host" }}

func GetPathOr

func GetPathOr(m interface{}, path string, defaultValue interface{}) interface{}

GetPathOr safely gets a nested value with a default fallback Usage: {{ getPathOr .Config "database.connection.host" "localhost" }}

func GroupBy

func GroupBy(slice interface{}, field string) map[interface{}][]interface{}

GroupBy groups a slice by a field value Usage: {{ $grouped := groupBy .Tables "Schema" }}

func Has

func Has(m interface{}, key interface{}) bool

Has checks if a key exists in a map Usage: {{ if has .Metadata "key" }}...{{ end }}

func HasPath

func HasPath(m interface{}, path string) bool

HasPath checks if a nested path exists Usage: {{ if hasPath .Config "database.connection.host" }}...{{ end }}

func HasPrefix

func HasPrefix(s, prefix string) bool

HasPrefix checks if string starts with prefix

func HasSuffix

func HasSuffix(s, suffix string) bool

HasSuffix checks if string ends with suffix

func Indent

func Indent(s string, spaces int) string

Indent indents each line of a string by the specified number of spaces Usage: {{ .Column.Description | indent 4 }}

func IndentWith

func IndentWith(s string, prefix string) string

IndentWith indents each line of a string with a custom prefix Usage: {{ .Column.Description | indentWith " " }}

func IndexOf

func IndexOf(slice interface{}, value interface{}) int

IndexOf returns the index of a value in a slice, or -1 if not found Usage: {{ $idx := indexOf .Names "admin" }}

func Join

func Join(parts []string, sep string) string

Join joins string slice with separator

func Keys

func Keys(m interface{}) []interface{}

Keys returns all keys from a map Usage: {{ range keys .Metadata }}...{{ end }}

func Last

func Last(slice interface{}, n int) []interface{}

Last returns the last N items from a slice Usage: {{ range last .Tables 5 }}...{{ end }}

func Merge

func Merge(maps ...interface{}) map[interface{}]interface{}

Merge merges multiple maps into a new map Usage: {{ $merged := merge .Map1 .Map2 }}

func Omit

func Omit(m interface{}, keys ...interface{}) map[interface{}]interface{}

Omit returns a new map without the specified keys Usage: {{ $filtered := omit .Metadata "internal" "private" }}

func Pick

func Pick(m interface{}, keys ...interface{}) map[interface{}]interface{}

Pick returns a new map with only the specified keys Usage: {{ $subset := pick .Metadata "name" "description" }}

func Pluck

func Pluck(slice interface{}, field string) []interface{}

Pluck extracts a field from each element in a slice Usage: {{ $names := pluck .Tables "Name" }}

func Pluralize

func Pluralize(s string) string

Pluralize converts a singular word to plural Basic implementation with common English rules

func QuoteString

func QuoteString(s string) string

QuoteString adds quotes around a string Usage: {{ .Column.Default | quoteString }}

func Replace

func Replace(s, old, newstr string, n int) string

Replace replaces occurrences of old with new (n times, or all if n < 0)

func Reverse

func Reverse(slice interface{}) []interface{}

Reverse reverses a slice Usage: {{ range reverse .Tables }}...{{ end }}

func SQLToCSharp

func SQLToCSharp(sqlType string, nullable bool) string

SQLToCSharp converts SQL types to C# types

func SQLToGo

func SQLToGo(sqlType string, nullable bool) string

SQLToGo converts SQL types to Go types

func SQLToJava

func SQLToJava(sqlType string, nullable bool) string

SQLToJava converts SQL types to Java types

func SQLToPhp

func SQLToPhp(sqlType string, nullable bool) string

SQLToPhp converts SQL types to PHP types

func SQLToPython

func SQLToPython(sqlType string) string

SQLToPython converts SQL types to Python types

func SQLToRust

func SQLToRust(sqlType string, nullable bool) string

SQLToRust converts SQL types to Rust types

func SQLToTypeScript

func SQLToTypeScript(sqlType string, nullable bool) string

SQLToTypeScript converts SQL types to TypeScript types

func SafeIndex

func SafeIndex(slice interface{}, index int) interface{}

SafeIndex safely gets an element from a slice by index Usage: {{ safeIndex .Tables 0 }}

func SafeIndexOr

func SafeIndexOr(slice interface{}, index int, defaultValue interface{}) interface{}

SafeIndexOr safely gets an element from a slice with a default fallback Usage: {{ safeIndexOr .Tables 0 "default" }}

func Singularize

func Singularize(s string) string

Singularize converts a plural word to singular Basic implementation with common English rules

func Skip

func Skip(slice interface{}, n int) []interface{}

Skip skips the first N items and returns the rest Usage: {{ range skip .Tables 2 }}...{{ end }}

func SliceContains

func SliceContains(slice interface{}, value interface{}) bool

SliceContains checks if a slice contains a value Usage: {{ if sliceContains .Names "admin" }}...{{ end }}

func SortBy

func SortBy(slice interface{}, field string) []interface{}

SortBy sorts a slice by a field name (for structs) or key (for maps) Usage: {{ $sorted := sortBy .Tables "Name" }} Note: This is a basic implementation that works for simple cases

func Split

func Split(s, sep string) []string

Split splits string by separator

func StringContains

func StringContains(s, substr string) bool

StringContains checks if substr is within s

func Take

func Take(slice interface{}, n int) []interface{}

Take returns the first N items (alias for First) Usage: {{ range take .Tables 5 }}...{{ end }}

func Title

func Title(s string) string

Title capitalizes the first letter of each word

func ToCamelCase

func ToCamelCase(s string) string

ToCamelCase converts snake_case to camelCase Examples: user_name → userName, http_request → httpRequest

func ToJSON

func ToJSON(v interface{}) string

ToJSON converts a value to JSON string Usage: {{ .Database | toJSON }}

func ToJSONPretty

func ToJSONPretty(v interface{}, indent string) string

ToJSONPretty converts a value to pretty-printed JSON string Usage: {{ .Database | toJSONPretty " " }}

func ToKebabCase

func ToKebabCase(s string) string

ToKebabCase converts snake_case or PascalCase/camelCase to kebab-case Examples: user_name → user-name, UserName → user-name

func ToLower

func ToLower(s string) string

ToLower converts a string to lowercase

func ToPascalCase

func ToPascalCase(s string) string

ToPascalCase converts snake_case to PascalCase Examples: user_name → UserName, http_request → HTTPRequest

func ToSnakeCase

func ToSnakeCase(s string) string

ToSnakeCase converts PascalCase/camelCase to snake_case Examples: UserName → user_name, HTTPRequest → http_request

func ToUpper

func ToUpper(s string) string

ToUpper converts a string to uppercase

func ToYAML

func ToYAML(v interface{}) string

ToYAML converts a value to YAML string Usage: {{ .Database | toYAML }}

func Trim

func Trim(s string) string

Trim trims whitespace from both ends

func TrimPrefix

func TrimPrefix(s, prefix string) string

TrimPrefix removes the prefix from the string if present

func TrimSuffix

func TrimSuffix(s, suffix string) string

TrimSuffix removes the suffix from the string if present

func Unique

func Unique(slice interface{}) []interface{}

Unique removes duplicates from a slice (compares by string representation) Usage: {{ $unique := unique .Items }}

func UnquoteString

func UnquoteString(s string) string

UnquoteString removes quotes from a string Usage: {{ .Value | unquoteString }}

func Values

func Values(m interface{}) []interface{}

Values returns all values from a map Usage: {{ range values .Table.Columns }}...{{ end }}

Types

type EntrypointMode

type EntrypointMode string

EntrypointMode defines how the template is executed

const (
	// DatabaseMode executes the template once for the entire database (single output)
	DatabaseMode EntrypointMode = "database"
	// SchemaMode executes the template once per schema (multi-file output)
	SchemaMode EntrypointMode = "schema"
	// ScriptMode executes the template once per script (multi-file output)
	ScriptMode EntrypointMode = "script"
	// TableMode executes the template once per table (multi-file output)
	TableMode EntrypointMode = "table"
)

type EnumeratedItem

type EnumeratedItem struct {
	Index int
	Value interface{}
}

EnumeratedItem represents an item with its index

func Enumerate

func Enumerate(slice interface{}) []EnumeratedItem

Enumerate returns a slice with index-value pairs Usage: {{ range enumerate .Tables }}{{ .Index }}: {{ .Value.Name }}{{ end }}

type TemplateData

type TemplateData struct {
	// One of these will be populated based on execution mode
	Database *models.Database
	Schema   *models.Schema
	Script   *models.Script
	Table    *models.Table

	// Context information (parent references)
	ParentSchema   *models.Schema   // Set for table/script modes
	ParentDatabase *models.Database // Always set for full database context

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

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

TemplateData wraps the model data with additional context for template execution

func NewDatabaseData

func NewDatabaseData(db *models.Database, metadata map[string]interface{}) *TemplateData

NewDatabaseData creates template data for database mode

func NewSchemaData

func NewSchemaData(schema *models.Schema, metadata map[string]interface{}) *TemplateData

NewSchemaData creates template data for schema mode

func NewScriptData

func NewScriptData(script *models.Script, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData

NewScriptData creates template data for script mode

func NewTableData

func NewTableData(table *models.Table, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData

NewTableData creates template data for table mode

func (*TemplateData) Name

func (td *TemplateData) Name() string

Name returns the primary name for the current template data (used for filename generation)

type TemplateError

type TemplateError struct {
	Phase   string // "load", "parse", "execute"
	Message string
	Err     error
}

TemplateError represents an error that occurred during template operations

func NewTemplateExecuteError

func NewTemplateExecuteError(msg string, err error) *TemplateError

NewTemplateExecuteError creates a new template execution error

func NewTemplateLoadError

func NewTemplateLoadError(msg string, err error) *TemplateError

NewTemplateLoadError creates a new template load error

func NewTemplateParseError

func NewTemplateParseError(msg string, err error) *TemplateError

NewTemplateParseError creates a new template parse error

func (*TemplateError) Error

func (e *TemplateError) Error() string

Error implements the error interface

func (*TemplateError) Unwrap

func (e *TemplateError) Unwrap() error

Unwrap returns the wrapped error

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer implements the writers.Writer interface for template-based output

func NewWriter

func NewWriter(options *writers.WriterOptions) (*Writer, error)

NewWriter creates a new template writer with the given options

func (*Writer) WriteDatabase

func (w *Writer) WriteDatabase(db *models.Database) error

WriteDatabase writes a database using the template

func (*Writer) WriteSchema

func (w *Writer) WriteSchema(schema *models.Schema) error

WriteSchema writes a schema using the template

func (*Writer) WriteTable

func (w *Writer) WriteTable(table *models.Table) error

WriteTable writes a single table using the template

Jump to

Keyboard shortcuts

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