feat(templ): added templ to command line that reads go template and outputs code

This commit is contained in:
2026-01-03 20:28:56 +02:00
parent 64aeac972a
commit fca7c99d74
24 changed files with 3955 additions and 0 deletions

74
pkg/commontypes/csharp.go Normal file
View File

@@ -0,0 +1,74 @@
package commontypes
import "strings"
// CSharpTypeMap maps PostgreSQL types to C# types
var CSharpTypeMap = map[string]string{
// Integer types
"integer": "int",
"int": "int",
"int4": "int",
"smallint": "short",
"int2": "short",
"bigint": "long",
"int8": "long",
"serial": "int",
"bigserial": "long",
"smallserial": "short",
// String types
"text": "string",
"varchar": "string",
"char": "string",
"character": "string",
"citext": "string",
"bpchar": "string",
"uuid": "Guid",
// Boolean
"boolean": "bool",
"bool": "bool",
// Float types
"real": "float",
"float4": "float",
"double precision": "double",
"float8": "double",
"numeric": "decimal",
"decimal": "decimal",
// Date/Time types
"timestamp": "DateTime",
"timestamp without time zone": "DateTime",
"timestamp with time zone": "DateTimeOffset",
"timestamptz": "DateTimeOffset",
"date": "DateTime",
"time": "TimeSpan",
"time without time zone": "TimeSpan",
"time with time zone": "DateTimeOffset",
"timetz": "DateTimeOffset",
// Binary
"bytea": "byte[]",
// JSON
"json": "string",
"jsonb": "string",
}
// SQLToCSharp converts SQL types to C# types
func SQLToCSharp(sqlType string, nullable bool) string {
baseType := ExtractBaseType(sqlType)
csType, ok := CSharpTypeMap[baseType]
if !ok {
csType = "object"
}
// Handle nullable value types (reference types are already nullable)
if !nullable && csType != "string" && !strings.HasSuffix(csType, "[]") && csType != "object" {
return csType + "?"
}
return csType
}

89
pkg/commontypes/golang.go Normal file
View File

@@ -0,0 +1,89 @@
package commontypes
import "strings"
// GoTypeMap maps PostgreSQL types to Go types
var GoTypeMap = map[string]string{
// Integer types
"integer": "int32",
"int": "int32",
"int4": "int32",
"smallint": "int16",
"int2": "int16",
"bigint": "int64",
"int8": "int64",
"serial": "int32",
"bigserial": "int64",
"smallserial": "int16",
// String types
"text": "string",
"varchar": "string",
"char": "string",
"character": "string",
"citext": "string",
"bpchar": "string",
// Boolean
"boolean": "bool",
"bool": "bool",
// Float types
"real": "float32",
"float4": "float32",
"double precision": "float64",
"float8": "float64",
"numeric": "float64",
"decimal": "float64",
// Date/Time types
"timestamp": "time.Time",
"timestamp without time zone": "time.Time",
"timestamp with time zone": "time.Time",
"timestamptz": "time.Time",
"date": "time.Time",
"time": "time.Time",
"time without time zone": "time.Time",
"time with time zone": "time.Time",
"timetz": "time.Time",
// Binary
"bytea": "[]byte",
// UUID
"uuid": "string",
// JSON
"json": "string",
"jsonb": "string",
// Array
"array": "[]string",
}
// SQLToGo converts SQL types to Go types
func SQLToGo(sqlType string, nullable bool) string {
baseType := ExtractBaseType(sqlType)
goType, ok := GoTypeMap[baseType]
if !ok {
goType = "interface{}"
}
// Handle nullable types
if nullable {
return goType
}
// For nullable, use pointer types (except for slices and interfaces)
if !strings.HasPrefix(goType, "[]") && goType != "interface{}" {
return "*" + goType
}
return goType
}
// NeedsTimeImport checks if a Go type requires the time package
func NeedsTimeImport(goType string) bool {
return strings.Contains(goType, "time.Time")
}

68
pkg/commontypes/java.go Normal file
View File

@@ -0,0 +1,68 @@
package commontypes
// JavaTypeMap maps PostgreSQL types to Java types
var JavaTypeMap = map[string]string{
// Integer types
"integer": "Integer",
"int": "Integer",
"int4": "Integer",
"smallint": "Short",
"int2": "Short",
"bigint": "Long",
"int8": "Long",
"serial": "Integer",
"bigserial": "Long",
"smallserial": "Short",
// String types
"text": "String",
"varchar": "String",
"char": "String",
"character": "String",
"citext": "String",
"bpchar": "String",
"uuid": "UUID",
// Boolean
"boolean": "Boolean",
"bool": "Boolean",
// Float types
"real": "Float",
"float4": "Float",
"double precision": "Double",
"float8": "Double",
"numeric": "BigDecimal",
"decimal": "BigDecimal",
// Date/Time types
"timestamp": "Timestamp",
"timestamp without time zone": "Timestamp",
"timestamp with time zone": "Timestamp",
"timestamptz": "Timestamp",
"date": "Date",
"time": "Time",
"time without time zone": "Time",
"time with time zone": "Time",
"timetz": "Time",
// Binary
"bytea": "byte[]",
// JSON
"json": "String",
"jsonb": "String",
}
// SQLToJava converts SQL types to Java types
func SQLToJava(sqlType string, nullable bool) string {
baseType := ExtractBaseType(sqlType)
javaType, ok := JavaTypeMap[baseType]
if !ok {
javaType = "Object"
}
// Java uses wrapper classes for nullable types by default
return javaType
}

72
pkg/commontypes/php.go Normal file
View File

@@ -0,0 +1,72 @@
package commontypes
// PHPTypeMap maps PostgreSQL types to PHP types
var PHPTypeMap = map[string]string{
// Integer types
"integer": "int",
"int": "int",
"int4": "int",
"smallint": "int",
"int2": "int",
"bigint": "int",
"int8": "int",
"serial": "int",
"bigserial": "int",
"smallserial": "int",
// String types
"text": "string",
"varchar": "string",
"char": "string",
"character": "string",
"citext": "string",
"bpchar": "string",
"uuid": "string",
// Boolean
"boolean": "bool",
"bool": "bool",
// Float types
"real": "float",
"float4": "float",
"double precision": "float",
"float8": "float",
"numeric": "float",
"decimal": "float",
// Date/Time types
"timestamp": "\\DateTime",
"timestamp without time zone": "\\DateTime",
"timestamp with time zone": "\\DateTime",
"timestamptz": "\\DateTime",
"date": "\\DateTime",
"time": "\\DateTime",
"time without time zone": "\\DateTime",
"time with time zone": "\\DateTime",
"timetz": "\\DateTime",
// Binary
"bytea": "string",
// JSON
"json": "array",
"jsonb": "array",
}
// SQLToPhp converts SQL types to PHP types
func SQLToPhp(sqlType string, nullable bool) string {
baseType := ExtractBaseType(sqlType)
phpType, ok := PHPTypeMap[baseType]
if !ok {
phpType = "mixed"
}
// PHP 7.1+ supports nullable types with ?Type syntax
if !nullable && phpType != "mixed" {
return "?" + phpType
}
return phpType
}

71
pkg/commontypes/python.go Normal file
View File

@@ -0,0 +1,71 @@
package commontypes
// PythonTypeMap maps PostgreSQL types to Python types
var PythonTypeMap = map[string]string{
// Integer types
"integer": "int",
"int": "int",
"int4": "int",
"smallint": "int",
"int2": "int",
"bigint": "int",
"int8": "int",
"serial": "int",
"bigserial": "int",
"smallserial": "int",
// String types
"text": "str",
"varchar": "str",
"char": "str",
"character": "str",
"citext": "str",
"bpchar": "str",
"uuid": "UUID",
// Boolean
"boolean": "bool",
"bool": "bool",
// Float types
"real": "float",
"float4": "float",
"double precision": "float",
"float8": "float",
"numeric": "Decimal",
"decimal": "Decimal",
// Date/Time types
"timestamp": "datetime",
"timestamp without time zone": "datetime",
"timestamp with time zone": "datetime",
"timestamptz": "datetime",
"date": "date",
"time": "time",
"time without time zone": "time",
"time with time zone": "time",
"timetz": "time",
// Binary
"bytea": "bytes",
// JSON
"json": "dict",
"jsonb": "dict",
// Array
"array": "list",
}
// SQLToPython converts SQL types to Python types
func SQLToPython(sqlType string) string {
baseType := ExtractBaseType(sqlType)
pyType, ok := PythonTypeMap[baseType]
if !ok {
pyType = "Any"
}
// Python uses Optional[Type] for nullable, but we return the base type
return pyType
}

72
pkg/commontypes/rust.go Normal file
View File

@@ -0,0 +1,72 @@
package commontypes
// RustTypeMap maps PostgreSQL types to Rust types
var RustTypeMap = map[string]string{
// Integer types
"integer": "i32",
"int": "i32",
"int4": "i32",
"smallint": "i16",
"int2": "i16",
"bigint": "i64",
"int8": "i64",
"serial": "i32",
"bigserial": "i64",
"smallserial": "i16",
// String types
"text": "String",
"varchar": "String",
"char": "String",
"character": "String",
"citext": "String",
"bpchar": "String",
"uuid": "String",
// Boolean
"boolean": "bool",
"bool": "bool",
// Float types
"real": "f32",
"float4": "f32",
"double precision": "f64",
"float8": "f64",
"numeric": "f64",
"decimal": "f64",
// Date/Time types (using chrono crate)
"timestamp": "NaiveDateTime",
"timestamp without time zone": "NaiveDateTime",
"timestamp with time zone": "DateTime<Utc>",
"timestamptz": "DateTime<Utc>",
"date": "NaiveDate",
"time": "NaiveTime",
"time without time zone": "NaiveTime",
"time with time zone": "DateTime<Utc>",
"timetz": "DateTime<Utc>",
// Binary
"bytea": "Vec<u8>",
// JSON
"json": "serde_json::Value",
"jsonb": "serde_json::Value",
}
// SQLToRust converts SQL types to Rust types
func SQLToRust(sqlType string, nullable bool) string {
baseType := ExtractBaseType(sqlType)
rustType, ok := RustTypeMap[baseType]
if !ok {
rustType = "String"
}
// Handle nullable types with Option<T>
if nullable {
return rustType
}
return "Option<" + rustType + ">"
}

22
pkg/commontypes/sql.go Normal file
View File

@@ -0,0 +1,22 @@
package commontypes
import "strings"
// ExtractBaseType extracts the base type from a SQL type string
// Examples: varchar(100) → varchar, numeric(10,2) → numeric
func ExtractBaseType(sqlType string) string {
sqlType = strings.ToLower(strings.TrimSpace(sqlType))
// Remove everything after '('
if idx := strings.Index(sqlType, "("); idx > 0 {
sqlType = sqlType[:idx]
}
return sqlType
}
// NormalizeType normalizes a SQL type to its base form
// Alias for ExtractBaseType for backwards compatibility
func NormalizeType(sqlType string) string {
return ExtractBaseType(sqlType)
}

View File

@@ -0,0 +1,75 @@
package commontypes
// TypeScriptTypeMap maps PostgreSQL types to TypeScript types
var TypeScriptTypeMap = map[string]string{
// Integer types
"integer": "number",
"int": "number",
"int4": "number",
"smallint": "number",
"int2": "number",
"bigint": "number",
"int8": "number",
"serial": "number",
"bigserial": "number",
"smallserial": "number",
// String types
"text": "string",
"varchar": "string",
"char": "string",
"character": "string",
"citext": "string",
"bpchar": "string",
"uuid": "string",
// Boolean
"boolean": "boolean",
"bool": "boolean",
// Float types
"real": "number",
"float4": "number",
"double precision": "number",
"float8": "number",
"numeric": "number",
"decimal": "number",
// Date/Time types
"timestamp": "Date",
"timestamp without time zone": "Date",
"timestamp with time zone": "Date",
"timestamptz": "Date",
"date": "Date",
"time": "Date",
"time without time zone": "Date",
"time with time zone": "Date",
"timetz": "Date",
// Binary
"bytea": "Buffer",
// JSON
"json": "any",
"jsonb": "any",
// Array
"array": "any[]",
}
// SQLToTypeScript converts SQL types to TypeScript types
func SQLToTypeScript(sqlType string, nullable bool) string {
baseType := ExtractBaseType(sqlType)
tsType, ok := TypeScriptTypeMap[baseType]
if !ok {
tsType = "any"
}
// Handle nullable types
if nullable {
return tsType
}
return tsType + " | null"
}

266
pkg/models/sorting.go Normal file
View File

@@ -0,0 +1,266 @@
package models
import (
"sort"
"strings"
)
// SortOrder represents the sort direction
type SortOrder bool
const (
// Ascending sort order
Ascending SortOrder = false
// Descending sort order
Descending SortOrder = true
)
// Schema Sorting
// SortSchemasByName sorts schemas by name
func SortSchemasByName(schemas []*Schema, desc bool) {
sort.SliceStable(schemas, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(schemas[i].Name), strings.ToLower(schemas[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// SortSchemasBySequence sorts schemas by sequence number
func SortSchemasBySequence(schemas []*Schema, desc bool) {
sort.SliceStable(schemas, func(i, j int) bool {
if desc {
return schemas[i].Sequence > schemas[j].Sequence
}
return schemas[i].Sequence < schemas[j].Sequence
})
}
// Table Sorting
// SortTablesByName sorts tables by name
func SortTablesByName(tables []*Table, desc bool) {
sort.SliceStable(tables, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(tables[i].Name), strings.ToLower(tables[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// SortTablesBySequence sorts tables by sequence number
func SortTablesBySequence(tables []*Table, desc bool) {
sort.SliceStable(tables, func(i, j int) bool {
if desc {
return tables[i].Sequence > tables[j].Sequence
}
return tables[i].Sequence < tables[j].Sequence
})
}
// Column Sorting
// SortColumnsMapByName converts column map to sorted slice by name
func SortColumnsMapByName(columns map[string]*Column, desc bool) []*Column {
result := make([]*Column, 0, len(columns))
for _, col := range columns {
result = append(result, col)
}
SortColumnsByName(result, desc)
return result
}
// SortColumnsMapBySequence converts column map to sorted slice by sequence
func SortColumnsMapBySequence(columns map[string]*Column, desc bool) []*Column {
result := make([]*Column, 0, len(columns))
for _, col := range columns {
result = append(result, col)
}
SortColumnsBySequence(result, desc)
return result
}
// SortColumnsByName sorts columns by name
func SortColumnsByName(columns []*Column, desc bool) {
sort.SliceStable(columns, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(columns[i].Name), strings.ToLower(columns[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// SortColumnsBySequence sorts columns by sequence number
func SortColumnsBySequence(columns []*Column, desc bool) {
sort.SliceStable(columns, func(i, j int) bool {
if desc {
return columns[i].Sequence > columns[j].Sequence
}
return columns[i].Sequence < columns[j].Sequence
})
}
// View Sorting
// SortViewsByName sorts views by name
func SortViewsByName(views []*View, desc bool) {
sort.SliceStable(views, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(views[i].Name), strings.ToLower(views[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// SortViewsBySequence sorts views by sequence number
func SortViewsBySequence(views []*View, desc bool) {
sort.SliceStable(views, func(i, j int) bool {
if desc {
return views[i].Sequence > views[j].Sequence
}
return views[i].Sequence < views[j].Sequence
})
}
// Sequence Sorting
// SortSequencesByName sorts sequences by name
func SortSequencesByName(sequences []*Sequence, desc bool) {
sort.SliceStable(sequences, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(sequences[i].Name), strings.ToLower(sequences[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// SortSequencesBySequence sorts sequences by sequence number
func SortSequencesBySequence(sequences []*Sequence, desc bool) {
sort.SliceStable(sequences, func(i, j int) bool {
if desc {
return sequences[i].Sequence > sequences[j].Sequence
}
return sequences[i].Sequence < sequences[j].Sequence
})
}
// Index Sorting
// SortIndexesMapByName converts index map to sorted slice by name
func SortIndexesMapByName(indexes map[string]*Index, desc bool) []*Index {
result := make([]*Index, 0, len(indexes))
for _, idx := range indexes {
result = append(result, idx)
}
SortIndexesByName(result, desc)
return result
}
// SortIndexesMapBySequence converts index map to sorted slice by sequence
func SortIndexesMapBySequence(indexes map[string]*Index, desc bool) []*Index {
result := make([]*Index, 0, len(indexes))
for _, idx := range indexes {
result = append(result, idx)
}
SortIndexesBySequence(result, desc)
return result
}
// SortIndexesByName sorts indexes by name
func SortIndexesByName(indexes []*Index, desc bool) {
sort.SliceStable(indexes, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(indexes[i].Name), strings.ToLower(indexes[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// SortIndexesBySequence sorts indexes by sequence number
func SortIndexesBySequence(indexes []*Index, desc bool) {
sort.SliceStable(indexes, func(i, j int) bool {
if desc {
return indexes[i].Sequence > indexes[j].Sequence
}
return indexes[i].Sequence < indexes[j].Sequence
})
}
// Constraint Sorting
// SortConstraintsMapByName converts constraint map to sorted slice by name
func SortConstraintsMapByName(constraints map[string]*Constraint, desc bool) []*Constraint {
result := make([]*Constraint, 0, len(constraints))
for _, c := range constraints {
result = append(result, c)
}
SortConstraintsByName(result, desc)
return result
}
// SortConstraintsByName sorts constraints by name
func SortConstraintsByName(constraints []*Constraint, desc bool) {
sort.SliceStable(constraints, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(constraints[i].Name), strings.ToLower(constraints[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// Relationship Sorting
// SortRelationshipsMapByName converts relationship map to sorted slice by name
func SortRelationshipsMapByName(relationships map[string]*Relationship, desc bool) []*Relationship {
result := make([]*Relationship, 0, len(relationships))
for _, r := range relationships {
result = append(result, r)
}
SortRelationshipsByName(result, desc)
return result
}
// SortRelationshipsByName sorts relationships by name
func SortRelationshipsByName(relationships []*Relationship, desc bool) {
sort.SliceStable(relationships, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(relationships[i].Name), strings.ToLower(relationships[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// Script Sorting
// SortScriptsByName sorts scripts by name
func SortScriptsByName(scripts []*Script, desc bool) {
sort.SliceStable(scripts, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(scripts[i].Name), strings.ToLower(scripts[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}
// Enum Sorting
// SortEnumsByName sorts enums by name
func SortEnumsByName(enums []*Enum, desc bool) {
sort.SliceStable(enums, func(i, j int) bool {
cmp := strings.Compare(strings.ToLower(enums[i].Name), strings.ToLower(enums[j].Name))
if desc {
return cmp > 0
}
return cmp < 0
})
}

326
pkg/reflectutil/helpers.go Normal file
View File

@@ -0,0 +1,326 @@
package reflectutil
import (
"reflect"
"strings"
)
// Deref dereferences pointers until it reaches a non-pointer value
// Returns the dereferenced value and true if successful, or the original value and false if nil
func Deref(v reflect.Value) (reflect.Value, bool) {
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return v, false
}
v = v.Elem()
}
return v, true
}
// DerefInterface dereferences an interface{} until it reaches a non-pointer value
func DerefInterface(i interface{}) reflect.Value {
v := reflect.ValueOf(i)
v, _ = Deref(v)
return v
}
// GetFieldValue extracts a field value from a struct, map, or pointer
// Returns nil if the field doesn't exist or can't be accessed
func GetFieldValue(item interface{}, field string) interface{} {
v := reflect.ValueOf(item)
v, ok := Deref(v)
if !ok {
return nil
}
switch v.Kind() {
case reflect.Struct:
fieldVal := v.FieldByName(field)
if fieldVal.IsValid() {
return fieldVal.Interface()
}
return nil
case reflect.Map:
keyVal := reflect.ValueOf(field)
mapVal := v.MapIndex(keyVal)
if mapVal.IsValid() {
return mapVal.Interface()
}
return nil
default:
return nil
}
}
// IsSliceOrArray checks if an interface{} is a slice or array
func IsSliceOrArray(i interface{}) bool {
v := reflect.ValueOf(i)
v, ok := Deref(v)
if !ok {
return false
}
k := v.Kind()
return k == reflect.Slice || k == reflect.Array
}
// IsMap checks if an interface{} is a map
func IsMap(i interface{}) bool {
v := reflect.ValueOf(i)
v, ok := Deref(v)
if !ok {
return false
}
return v.Kind() == reflect.Map
}
// SliceLen returns the length of a slice/array, or 0 if not a slice/array
func SliceLen(i interface{}) int {
v := reflect.ValueOf(i)
v, ok := Deref(v)
if !ok {
return 0
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return 0
}
return v.Len()
}
// MapLen returns the length of a map, or 0 if not a map
func MapLen(i interface{}) int {
v := reflect.ValueOf(i)
v, ok := Deref(v)
if !ok {
return 0
}
if v.Kind() != reflect.Map {
return 0
}
return v.Len()
}
// SliceToInterfaces converts a slice/array to []interface{}
// Returns empty slice if not a slice/array
func SliceToInterfaces(i interface{}) []interface{} {
v := reflect.ValueOf(i)
v, ok := Deref(v)
if !ok {
return []interface{}{}
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
result := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
result[i] = v.Index(i).Interface()
}
return result
}
// MapKeys returns all keys from a map as []interface{}
// Returns empty slice if not a map
func MapKeys(i interface{}) []interface{} {
v := reflect.ValueOf(i)
v, ok := Deref(v)
if !ok {
return []interface{}{}
}
if v.Kind() != reflect.Map {
return []interface{}{}
}
keys := v.MapKeys()
result := make([]interface{}, len(keys))
for i, key := range keys {
result[i] = key.Interface()
}
return result
}
// MapValues returns all values from a map as []interface{}
// Returns empty slice if not a map
func MapValues(i interface{}) []interface{} {
v := reflect.ValueOf(i)
v, ok := Deref(v)
if !ok {
return []interface{}{}
}
if v.Kind() != reflect.Map {
return []interface{}{}
}
result := make([]interface{}, 0, v.Len())
iter := v.MapRange()
for iter.Next() {
result = append(result, iter.Value().Interface())
}
return result
}
// MapGet safely gets a value from a map by key
// Returns nil if key doesn't exist or not a map
func MapGet(m interface{}, key interface{}) interface{} {
v := reflect.ValueOf(m)
v, ok := Deref(v)
if !ok {
return nil
}
if v.Kind() != reflect.Map {
return nil
}
keyVal := reflect.ValueOf(key)
mapVal := v.MapIndex(keyVal)
if mapVal.IsValid() {
return mapVal.Interface()
}
return nil
}
// SliceIndex safely gets an element from a slice/array by index
// Returns nil if index out of bounds or not a slice/array
func SliceIndex(slice interface{}, index int) interface{} {
v := reflect.ValueOf(slice)
v, ok := Deref(v)
if !ok {
return nil
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return nil
}
if index < 0 || index >= v.Len() {
return nil
}
return v.Index(index).Interface()
}
// CompareValues compares two values for sorting
// Returns -1 if a < b, 0 if a == b, 1 if a > b
func CompareValues(a, b interface{}) int {
if a == nil && b == nil {
return 0
}
if a == nil {
return -1
}
if b == nil {
return 1
}
va := reflect.ValueOf(a)
vb := reflect.ValueOf(b)
// Handle different types
switch va.Kind() {
case reflect.String:
if vb.Kind() == reflect.String {
as := va.String()
bs := vb.String()
if as < bs {
return -1
} else if as > bs {
return 1
}
return 0
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if vb.Kind() >= reflect.Int && vb.Kind() <= reflect.Int64 {
ai := va.Int()
bi := vb.Int()
if ai < bi {
return -1
} else if ai > bi {
return 1
}
return 0
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if vb.Kind() >= reflect.Uint && vb.Kind() <= reflect.Uint64 {
au := va.Uint()
bu := vb.Uint()
if au < bu {
return -1
} else if au > bu {
return 1
}
return 0
}
case reflect.Float32, reflect.Float64:
if vb.Kind() == reflect.Float32 || vb.Kind() == reflect.Float64 {
af := va.Float()
bf := vb.Float()
if af < bf {
return -1
} else if af > bf {
return 1
}
return 0
}
}
return 0
}
// GetNestedValue gets a nested value using dot notation path
// Example: GetNestedValue(obj, "database.schema.table")
func GetNestedValue(m interface{}, path string) interface{} {
if path == "" {
return m
}
parts := strings.Split(path, ".")
current := m
for _, part := range parts {
if current == nil {
return nil
}
v := reflect.ValueOf(current)
v, ok := Deref(v)
if !ok {
return nil
}
switch v.Kind() {
case reflect.Map:
keyVal := reflect.ValueOf(part)
mapVal := v.MapIndex(keyVal)
if !mapVal.IsValid() {
return nil
}
current = mapVal.Interface()
case reflect.Struct:
fieldVal := v.FieldByName(part)
if !fieldVal.IsValid() {
return nil
}
current = fieldVal.Interface()
default:
return nil
}
}
return current
}
// DeepEqual performs a deep equality check between two values
func DeepEqual(a, b interface{}) bool {
return reflect.DeepEqual(a, b)
}

View File

@@ -0,0 +1,276 @@
# 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](../../../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:
```go
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`:
```go
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:
```go
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:
```go
// string_helpers.go
func ToScreamingSnakeCase(s string) string {
return strings.ToUpper(ToSnakeCase(s))
}
```
2. **Register in funcmap.go:**
```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
```bash
# 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:
```go
// 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
```go
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
```bash
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
- [User Documentation](/docs/TEMPLATE_MODE.md) - Complete template function reference
- [Common Types Package](../../commontypes/) - Centralized type mappings
- [Reflect Utilities](../../reflectutil/) - Reflection helpers
- [Models Package](../../models/) - Database schema models
- [Go Template Docs](https://pkg.go.dev/text/template) - Official Go template documentation

View File

@@ -0,0 +1,50 @@
package template
import "fmt"
// TemplateError represents an error that occurred during template operations
type TemplateError struct {
Phase string // "load", "parse", "execute"
Message string
Err error
}
// Error implements the error interface
func (e *TemplateError) Error() string {
if e.Err != nil {
return fmt.Sprintf("template %s error: %s: %v", e.Phase, e.Message, e.Err)
}
return fmt.Sprintf("template %s error: %s", e.Phase, e.Message)
}
// Unwrap returns the wrapped error
func (e *TemplateError) Unwrap() error {
return e.Err
}
// NewTemplateLoadError creates a new template load error
func NewTemplateLoadError(msg string, err error) *TemplateError {
return &TemplateError{
Phase: "load",
Message: msg,
Err: err,
}
}
// NewTemplateParseError creates a new template parse error
func NewTemplateParseError(msg string, err error) *TemplateError {
return &TemplateError{
Phase: "parse",
Message: msg,
Err: err,
}
}
// NewTemplateExecuteError creates a new template execution error
func NewTemplateExecuteError(msg string, err error) *TemplateError {
return &TemplateError{
Phase: "execute",
Message: msg,
Err: err,
}
}

View File

@@ -0,0 +1,144 @@
package template
import (
"path/filepath"
"strings"
"git.warky.dev/wdevs/relspecgo/pkg/commontypes"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
// 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 FilterTables(tables []*models.Table, pattern string) []*models.Table {
if pattern == "" {
return tables
}
result := make([]*models.Table, 0)
for _, table := range tables {
if matchPattern(table.Name, pattern) {
result = append(result, table)
}
}
return result
}
// FilterTablesByPattern filters tables by name pattern (glob-style)
// Usage: {{ $userTables := filterTablesByPattern .Schema.Tables "user_*" }}
func FilterTablesByPattern(tables []*models.Table, pattern string) []*models.Table {
return FilterTables(tables, pattern)
}
// FilterColumns filters columns from a map using a pattern
// Usage: {{ $filtered := filterColumns .Table.Columns "*_id" }}
func FilterColumns(columns map[string]*models.Column, pattern string) []*models.Column {
result := make([]*models.Column, 0)
for _, col := range columns {
if pattern == "" || matchPattern(col.Name, pattern) {
result = append(result, col)
}
}
return result
}
// FilterColumnsByType filters columns by SQL type
// Usage: {{ $stringCols := filterColumnsByType .Table.Columns "varchar" }}
func FilterColumnsByType(columns map[string]*models.Column, sqlType string) []*models.Column {
result := make([]*models.Column, 0)
baseType := commontypes.ExtractBaseType(sqlType)
for _, col := range columns {
colBaseType := commontypes.ExtractBaseType(col.Type)
if colBaseType == baseType {
result = append(result, col)
}
}
return result
}
// FilterPrimaryKeys returns only columns that are primary keys
// Usage: {{ $pks := filterPrimaryKeys .Table.Columns }}
func FilterPrimaryKeys(columns map[string]*models.Column) []*models.Column {
result := make([]*models.Column, 0)
for _, col := range columns {
if col.IsPrimaryKey {
result = append(result, col)
}
}
return result
}
// FilterForeignKeys returns only foreign key constraints
// Usage: {{ $fks := filterForeignKeys .Table.Constraints }}
func FilterForeignKeys(constraints map[string]*models.Constraint) []*models.Constraint {
result := make([]*models.Constraint, 0)
for _, constraint := range constraints {
if constraint.Type == models.ForeignKeyConstraint {
result = append(result, constraint)
}
}
return result
}
// FilterUniqueConstraints returns only unique constraints
// Usage: {{ $uniques := filterUniqueConstraints .Table.Constraints }}
func FilterUniqueConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
result := make([]*models.Constraint, 0)
for _, constraint := range constraints {
if constraint.Type == models.UniqueConstraint {
result = append(result, constraint)
}
}
return result
}
// FilterCheckConstraints returns only check constraints
// Usage: {{ $checks := filterCheckConstraints .Table.Constraints }}
func FilterCheckConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
result := make([]*models.Constraint, 0)
for _, constraint := range constraints {
if constraint.Type == models.CheckConstraint {
result = append(result, constraint)
}
}
return result
}
// FilterNullable returns only nullable columns
// Usage: {{ $nullables := filterNullable .Table.Columns }}
func FilterNullable(columns map[string]*models.Column) []*models.Column {
result := make([]*models.Column, 0)
for _, col := range columns {
if !col.NotNull {
result = append(result, col)
}
}
return result
}
// FilterNotNull returns only non-nullable columns
// Usage: {{ $required := filterNotNull .Table.Columns }}
func FilterNotNull(columns map[string]*models.Column) []*models.Column {
result := make([]*models.Column, 0)
for _, col := range columns {
if col.NotNull {
result = append(result, col)
}
}
return result
}
// matchPattern performs simple glob-style pattern matching
// Supports: * (any characters), ? (single character)
// Examples: "user_*" matches "user_profile", "user_settings"
func matchPattern(s, pattern string) bool {
// Use filepath.Match for glob-style pattern matching
matched, err := filepath.Match(pattern, s)
if err != nil {
// If pattern is invalid, do exact match
return strings.EqualFold(s, pattern)
}
return matched
}

View File

@@ -0,0 +1,157 @@
package template
import (
"encoding/json"
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
// ToJSON converts a value to JSON string
// Usage: {{ .Database | toJSON }}
func ToJSON(v interface{}) string {
data, err := json.Marshal(v)
if err != nil {
return fmt.Sprintf("{\"error\": \"failed to marshal: %v\"}", err)
}
return string(data)
}
// ToJSONPretty converts a value to pretty-printed JSON string
// Usage: {{ .Database | toJSONPretty " " }}
func ToJSONPretty(v interface{}, indent string) string {
data, err := json.MarshalIndent(v, "", indent)
if err != nil {
return fmt.Sprintf("{\"error\": \"failed to marshal: %v\"}", err)
}
return string(data)
}
// ToYAML converts a value to YAML string
// Usage: {{ .Database | toYAML }}
func ToYAML(v interface{}) string {
data, err := yaml.Marshal(v)
if err != nil {
return fmt.Sprintf("error: failed to marshal: %v", err)
}
return string(data)
}
// Indent indents each line of a string by the specified number of spaces
// Usage: {{ .Column.Description | indent 4 }}
func Indent(s string, spaces int) string {
if s == "" {
return ""
}
prefix := strings.Repeat(" ", spaces)
lines := strings.Split(s, "\n")
for i, line := range lines {
if line != "" {
lines[i] = prefix + line
}
}
return strings.Join(lines, "\n")
}
// IndentWith indents each line of a string with a custom prefix
// Usage: {{ .Column.Description | indentWith " " }}
func IndentWith(s string, prefix string) string {
if s == "" {
return ""
}
lines := strings.Split(s, "\n")
for i, line := range lines {
if line != "" {
lines[i] = prefix + line
}
}
return strings.Join(lines, "\n")
}
// Escape escapes special characters in a string for use in code
// Usage: {{ .Column.Default | escape }}
func Escape(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "\"", "\\\"")
s = strings.ReplaceAll(s, "\n", "\\n")
s = strings.ReplaceAll(s, "\r", "\\r")
s = strings.ReplaceAll(s, "\t", "\\t")
return s
}
// EscapeQuotes escapes only quote characters
// Usage: {{ .Column.Comment | escapeQuotes }}
func EscapeQuotes(s string) string {
s = strings.ReplaceAll(s, "\"", "\\\"")
s = strings.ReplaceAll(s, "'", "\\'")
return s
}
// Comment adds comment prefix to a string
// Supports: "//" (Go, C++, etc.), "#" (Python, shell), "--" (SQL), "/* */" (block)
// Usage: {{ .Table.Description | comment "//" }}
func Comment(s string, style string) string {
if s == "" {
return ""
}
lines := strings.Split(s, "\n")
switch style {
case "//":
for i, line := range lines {
lines[i] = "// " + line
}
return strings.Join(lines, "\n")
case "#":
for i, line := range lines {
lines[i] = "# " + line
}
return strings.Join(lines, "\n")
case "--":
for i, line := range lines {
lines[i] = "-- " + line
}
return strings.Join(lines, "\n")
case "/* */", "/**/":
if len(lines) == 1 {
return "/* " + lines[0] + " */"
}
result := "/*\n"
for _, line := range lines {
result += " * " + line + "\n"
}
result += " */"
return result
default:
// Default to // style
for i, line := range lines {
lines[i] = "// " + line
}
return strings.Join(lines, "\n")
}
}
// QuoteString adds quotes around a string
// Usage: {{ .Column.Default | quoteString }}
func QuoteString(s string) string {
return "\"" + s + "\""
}
// UnquoteString removes quotes from a string
// Usage: {{ .Value | unquoteString }}
func UnquoteString(s string) string {
if len(s) >= 2 {
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
return s[1 : len(s)-1]
}
}
return s
}

View File

@@ -0,0 +1,171 @@
package template
import (
"text/template"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
// BuildFuncMap creates a template.FuncMap with all available helper functions
func BuildFuncMap() template.FuncMap {
return template.FuncMap{
// String manipulation functions
"toUpper": ToUpper,
"toLower": ToLower,
"toCamelCase": ToCamelCase,
"toPascalCase": ToPascalCase,
"toSnakeCase": ToSnakeCase,
"toKebabCase": ToKebabCase,
"pluralize": Pluralize,
"singularize": Singularize,
"title": Title,
"trim": Trim,
"trimPrefix": TrimPrefix,
"trimSuffix": TrimSuffix,
"replace": Replace,
"stringContains": StringContains, // Avoid conflict with slice contains
"hasPrefix": HasPrefix,
"hasSuffix": HasSuffix,
"split": Split,
"join": Join,
// Type conversion functions
"sqlToGo": SQLToGo,
"sqlToTypeScript": SQLToTypeScript,
"sqlToJava": SQLToJava,
"sqlToPython": SQLToPython,
"sqlToRust": SQLToRust,
"sqlToCSharp": SQLToCSharp,
"sqlToPhp": SQLToPhp,
// Filtering functions
"filterTables": FilterTables,
"filterTablesByPattern": FilterTablesByPattern,
"filterColumns": FilterColumns,
"filterColumnsByType": FilterColumnsByType,
"filterPrimaryKeys": FilterPrimaryKeys,
"filterForeignKeys": FilterForeignKeys,
"filterUniqueConstraints": FilterUniqueConstraints,
"filterCheckConstraints": FilterCheckConstraints,
"filterNullable": FilterNullable,
"filterNotNull": FilterNotNull,
// Formatting functions
"toJSON": ToJSON,
"toJSONPretty": ToJSONPretty,
"toYAML": ToYAML,
"indent": Indent,
"indentWith": IndentWith,
"escape": Escape,
"escapeQuotes": EscapeQuotes,
"comment": Comment,
"quoteString": QuoteString,
"unquoteString": UnquoteString,
// Loop/iteration helper functions
"enumerate": Enumerate,
"batch": Batch,
"chunk": Chunk,
"reverse": Reverse,
"first": First,
"last": Last,
"skip": Skip,
"take": Take,
"concat": Concat,
"unique": Unique,
"sortBy": SortBy,
"groupBy": GroupBy,
// Safe access functions
"get": Get,
"getOr": GetOr,
"getPath": GetPath,
"getPathOr": GetPathOr,
"safeIndex": SafeIndex,
"safeIndexOr": SafeIndexOr,
"has": Has,
"hasPath": HasPath,
"keys": Keys,
"values": Values,
"merge": Merge,
"pick": Pick,
"omit": Omit,
"sliceContains": SliceContains,
"indexOf": IndexOf,
"pluck": Pluck,
// Sorting functions
"sortSchemasByName": models.SortSchemasByName,
"sortSchemasBySequence": models.SortSchemasBySequence,
"sortTablesByName": models.SortTablesByName,
"sortTablesBySequence": models.SortTablesBySequence,
"sortColumnsByName": models.SortColumnsByName,
"sortColumnsBySequence": models.SortColumnsBySequence,
"sortColumnsMapByName": models.SortColumnsMapByName,
"sortColumnsMapBySequence": models.SortColumnsMapBySequence,
"sortViewsByName": models.SortViewsByName,
"sortViewsBySequence": models.SortViewsBySequence,
"sortSequencesByName": models.SortSequencesByName,
"sortSequencesBySequence": models.SortSequencesBySequence,
"sortIndexesByName": models.SortIndexesByName,
"sortIndexesBySequence": models.SortIndexesBySequence,
"sortIndexesMapByName": models.SortIndexesMapByName,
"sortIndexesMapBySequence": models.SortIndexesMapBySequence,
"sortConstraintsByName": models.SortConstraintsByName,
"sortConstraintsMapByName": models.SortConstraintsMapByName,
"sortRelationshipsByName": models.SortRelationshipsByName,
"sortRelationshipsMapByName": models.SortRelationshipsMapByName,
"sortScriptsByName": models.SortScriptsByName,
"sortEnumsByName": models.SortEnumsByName,
// Utility functions (built-in Go template helpers + custom)
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
"div": func(a, b int) int {
if b == 0 {
return 0
}
return a / b
},
"mod": func(a, b int) int {
if b == 0 {
return 0
}
return a % b
},
"default": func(defaultVal, val interface{}) interface{} {
if val == nil {
return defaultVal
}
return val
},
"dict": func(values ...interface{}) map[string]interface{} {
if len(values)%2 != 0 {
return nil
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil
}
dict[key] = values[i+1]
}
return dict
},
"list": func(values ...interface{}) []interface{} {
return values
},
"seq": func(start, end int) []int {
if start > end {
return []int{}
}
result := make([]int, end-start+1)
for i := range result {
result[i] = start + i
}
return result
},
}
}

View File

@@ -0,0 +1,282 @@
package template
import (
"reflect"
"sort"
"git.warky.dev/wdevs/relspecgo/pkg/reflectutil"
)
// EnumeratedItem represents an item with its index
type EnumeratedItem struct {
Index int
Value interface{}
}
// Enumerate returns a slice with index-value pairs
// Usage: {{ range enumerate .Tables }}{{ .Index }}: {{ .Value.Name }}{{ end }}
func Enumerate(slice interface{}) []EnumeratedItem {
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []EnumeratedItem{}
}
result := make([]EnumeratedItem, v.Len())
for i := 0; i < v.Len(); i++ {
result[i] = EnumeratedItem{
Index: i,
Value: v.Index(i).Interface(),
}
}
return result
}
// Batch splits a slice into chunks of specified size
// Usage: {{ range batch .Columns 3 }}...{{ end }}
func Batch(slice interface{}, size int) [][]interface{} {
if size <= 0 {
return [][]interface{}{}
}
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return [][]interface{}{}
}
length := v.Len()
if length == 0 {
return [][]interface{}{}
}
numBatches := (length + size - 1) / size
result := make([][]interface{}, numBatches)
for i := 0; i < numBatches; i++ {
start := i * size
end := start + size
if end > length {
end = length
}
batch := make([]interface{}, end-start)
for j := start; j < end; j++ {
batch[j-start] = v.Index(j).Interface()
}
result[i] = batch
}
return result
}
// Chunk is an alias for Batch
func Chunk(slice interface{}, size int) [][]interface{} {
return Batch(slice, size)
}
// Reverse reverses a slice
// Usage: {{ range reverse .Tables }}...{{ end }}
func Reverse(slice interface{}) []interface{} {
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
length := v.Len()
result := make([]interface{}, length)
for i := 0; i < length; i++ {
result[length-1-i] = v.Index(i).Interface()
}
return result
}
// First returns the first N items from a slice
// Usage: {{ range first .Tables 5 }}...{{ end }}
func First(slice interface{}, n int) []interface{} {
if n <= 0 {
return []interface{}{}
}
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
length := v.Len()
if n > length {
n = length
}
result := make([]interface{}, n)
for i := 0; i < n; i++ {
result[i] = v.Index(i).Interface()
}
return result
}
// Last returns the last N items from a slice
// Usage: {{ range last .Tables 5 }}...{{ end }}
func Last(slice interface{}, n int) []interface{} {
if n <= 0 {
return []interface{}{}
}
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
length := v.Len()
if n > length {
n = length
}
result := make([]interface{}, n)
start := length - n
for i := 0; i < n; i++ {
result[i] = v.Index(start + i).Interface()
}
return result
}
// Skip skips the first N items and returns the rest
// Usage: {{ range skip .Tables 2 }}...{{ end }}
func Skip(slice interface{}, n int) []interface{} {
if n < 0 {
n = 0
}
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
length := v.Len()
if n >= length {
return []interface{}{}
}
result := make([]interface{}, length-n)
for i := n; i < length; i++ {
result[i-n] = v.Index(i).Interface()
}
return result
}
// Take returns the first N items (alias for First)
// Usage: {{ range take .Tables 5 }}...{{ end }}
func Take(slice interface{}, n int) []interface{} {
return First(slice, n)
}
// Concat concatenates multiple slices
// Usage: {{ $all := concat .Schema1.Tables .Schema2.Tables }}
func Concat(slices ...interface{}) []interface{} {
result := make([]interface{}, 0)
for _, slice := range slices {
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
continue
}
for i := 0; i < v.Len(); i++ {
result = append(result, v.Index(i).Interface())
}
}
return result
}
// Unique removes duplicates from a slice (compares by string representation)
// Usage: {{ $unique := unique .Items }}
func Unique(slice interface{}) []interface{} {
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
seen := make(map[interface{}]bool)
result := make([]interface{}, 0)
for i := 0; i < v.Len(); i++ {
item := v.Index(i).Interface()
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// 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 SortBy(slice interface{}, field string) []interface{} {
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
// Convert to interface slice
result := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
result[i] = v.Index(i).Interface()
}
// Sort by field
sort.Slice(result, func(i, j int) bool {
vi := getFieldValue(result[i], field)
vj := getFieldValue(result[j], field)
return compareValues(vi, vj) < 0
})
return result
}
// GroupBy groups a slice by a field value
// Usage: {{ $grouped := groupBy .Tables "Schema" }}
func GroupBy(slice interface{}, field string) map[interface{}][]interface{} {
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return map[interface{}][]interface{}{}
}
result := make(map[interface{}][]interface{})
for i := 0; i < v.Len(); i++ {
item := v.Index(i).Interface()
key := getFieldValue(item, field)
result[key] = append(result[key], item)
}
return result
}
// CountIf counts items matching a condition
// Note: Since templates can't pass functions, this is limited
// Usage in code (not directly in templates)
func CountIf(slice interface{}, matches func(interface{}) bool) int {
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return 0
}
count := 0
for i := 0; i < v.Len(); i++ {
if matches(v.Index(i).Interface()) {
count++
}
}
return count
}
// getFieldValue extracts a field value from a struct, map, or pointer
func getFieldValue(item interface{}, field string) interface{} {
return reflectutil.GetFieldValue(item, field)
}
// compareValues compares two values for sorting
func compareValues(a, b interface{}) int {
return reflectutil.CompareValues(a, b)
}

View File

@@ -0,0 +1,288 @@
package template
import (
"reflect"
"git.warky.dev/wdevs/relspecgo/pkg/reflectutil"
)
// Get safely gets a value from a map by key
// Usage: {{ get .Metadata "key" }}
func Get(m interface{}, key interface{}) interface{} {
return reflectutil.MapGet(m, key)
}
// GetOr safely gets a value from a map with a default fallback
// Usage: {{ getOr .Metadata "key" "default" }}
func GetOr(m interface{}, key interface{}, defaultValue interface{}) interface{} {
result := Get(m, key)
if result == nil {
return defaultValue
}
return result
}
// GetPath safely gets a nested value using dot notation
// Usage: {{ getPath .Config "database.connection.host" }}
func GetPath(m interface{}, path string) interface{} {
return reflectutil.GetNestedValue(m, path)
}
// GetPathOr safely gets a nested value with a default fallback
// Usage: {{ getPathOr .Config "database.connection.host" "localhost" }}
func GetPathOr(m interface{}, path string, defaultValue interface{}) interface{} {
result := GetPath(m, path)
if result == nil {
return defaultValue
}
return result
}
// SafeIndex safely gets an element from a slice by index
// Usage: {{ safeIndex .Tables 0 }}
func SafeIndex(slice interface{}, index int) interface{} {
return reflectutil.SliceIndex(slice, index)
}
// SafeIndexOr safely gets an element from a slice with a default fallback
// Usage: {{ safeIndexOr .Tables 0 "default" }}
func SafeIndexOr(slice interface{}, index int, defaultValue interface{}) interface{} {
result := SafeIndex(slice, index)
if result == nil {
return defaultValue
}
return result
}
// Has checks if a key exists in a map
// Usage: {{ if has .Metadata "key" }}...{{ end }}
func Has(m interface{}, key interface{}) bool {
v := reflect.ValueOf(m)
// Dereference pointers
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return false
}
v = v.Elem()
}
if v.Kind() != reflect.Map {
return false
}
keyVal := reflect.ValueOf(key)
return v.MapIndex(keyVal).IsValid()
}
// HasPath checks if a nested path exists
// Usage: {{ if hasPath .Config "database.connection.host" }}...{{ end }}
func HasPath(m interface{}, path string) bool {
return GetPath(m, path) != nil
}
// Keys returns all keys from a map
// Usage: {{ range keys .Metadata }}...{{ end }}
func Keys(m interface{}) []interface{} {
return reflectutil.MapKeys(m)
}
// Values returns all values from a map
// Usage: {{ range values .Table.Columns }}...{{ end }}
func Values(m interface{}) []interface{} {
return reflectutil.MapValues(m)
}
// Merge merges multiple maps into a new map
// Usage: {{ $merged := merge .Map1 .Map2 }}
func Merge(maps ...interface{}) map[interface{}]interface{} {
result := make(map[interface{}]interface{})
for _, m := range maps {
v := reflect.ValueOf(m)
// Dereference pointers
for v.Kind() == reflect.Ptr {
if v.IsNil() {
continue
}
v = v.Elem()
}
if v.Kind() != reflect.Map {
continue
}
iter := v.MapRange()
for iter.Next() {
result[iter.Key().Interface()] = iter.Value().Interface()
}
}
return result
}
// Pick returns a new map with only the specified keys
// Usage: {{ $subset := pick .Metadata "name" "description" }}
func Pick(m interface{}, keys ...interface{}) map[interface{}]interface{} {
result := make(map[interface{}]interface{})
v := reflect.ValueOf(m)
// Dereference pointers
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return result
}
v = v.Elem()
}
if v.Kind() != reflect.Map {
return result
}
for _, key := range keys {
keyVal := reflect.ValueOf(key)
mapVal := v.MapIndex(keyVal)
if mapVal.IsValid() {
result[key] = mapVal.Interface()
}
}
return result
}
// Omit returns a new map without the specified keys
// Usage: {{ $filtered := omit .Metadata "internal" "private" }}
func Omit(m interface{}, keys ...interface{}) map[interface{}]interface{} {
result := make(map[interface{}]interface{})
v := reflect.ValueOf(m)
// Dereference pointers
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return result
}
v = v.Elem()
}
if v.Kind() != reflect.Map {
return result
}
// Create a set of keys to omit
omitSet := make(map[interface{}]bool)
for _, key := range keys {
omitSet[key] = true
}
// Add all keys that are not in the omit set
iter := v.MapRange()
for iter.Next() {
key := iter.Key().Interface()
if !omitSet[key] {
result[key] = iter.Value().Interface()
}
}
return result
}
// SliceContains checks if a slice contains a value
// Usage: {{ if sliceContains .Names "admin" }}...{{ end }}
func SliceContains(slice interface{}, value interface{}) bool {
v := reflect.ValueOf(slice)
v, ok := reflectutil.Deref(v)
if !ok {
return false
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return false
}
for i := 0; i < v.Len(); i++ {
if reflectutil.DeepEqual(v.Index(i).Interface(), value) {
return true
}
}
return false
}
// IndexOf returns the index of a value in a slice, or -1 if not found
// Usage: {{ $idx := indexOf .Names "admin" }}
func IndexOf(slice interface{}, value interface{}) int {
v := reflect.ValueOf(slice)
v, ok := reflectutil.Deref(v)
if !ok {
return -1
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return -1
}
for i := 0; i < v.Len(); i++ {
if reflectutil.DeepEqual(v.Index(i).Interface(), value) {
return i
}
}
return -1
}
// Pluck extracts a field from each element in a slice
// Usage: {{ $names := pluck .Tables "Name" }}
func Pluck(slice interface{}, field string) []interface{} {
v := reflect.ValueOf(slice)
// Dereference pointers
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return []interface{}{}
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return []interface{}{}
}
result := make([]interface{}, 0, v.Len())
for i := 0; i < v.Len(); i++ {
item := v.Index(i)
// Dereference item pointers
for item.Kind() == reflect.Ptr {
if item.IsNil() {
result = append(result, nil)
continue
}
item = item.Elem()
}
switch item.Kind() {
case reflect.Struct:
fieldVal := item.FieldByName(field)
if fieldVal.IsValid() {
result = append(result, fieldVal.Interface())
} else {
result = append(result, nil)
}
case reflect.Map:
keyVal := reflect.ValueOf(field)
mapVal := item.MapIndex(keyVal)
if mapVal.IsValid() {
result = append(result, mapVal.Interface())
} else {
result = append(result, nil)
}
default:
result = append(result, nil)
}
}
return result
}

View File

@@ -0,0 +1,316 @@
package template
import (
"strings"
"unicode"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// ToUpper converts a string to uppercase
func ToUpper(s string) string {
return strings.ToUpper(s)
}
// ToLower converts a string to lowercase
func ToLower(s string) string {
return strings.ToLower(s)
}
// ToCamelCase converts snake_case to camelCase
// Examples: user_name → userName, http_request → httpRequest
func ToCamelCase(s string) string {
if s == "" {
return ""
}
parts := strings.Split(s, "_")
for i, part := range parts {
if i == 0 {
parts[i] = strings.ToLower(part)
} else {
parts[i] = capitalize(part)
}
}
return strings.Join(parts, "")
}
// ToPascalCase converts snake_case to PascalCase
// Examples: user_name → UserName, http_request → HTTPRequest
func ToPascalCase(s string) string {
if s == "" {
return ""
}
parts := strings.Split(s, "_")
for i, part := range parts {
parts[i] = capitalize(part)
}
return strings.Join(parts, "")
}
// ToSnakeCase converts PascalCase/camelCase to snake_case
// Examples: UserName → user_name, HTTPRequest → http_request
func ToSnakeCase(s string) string {
if s == "" {
return ""
}
var result strings.Builder
var prevUpper bool
var nextUpper bool
runes := []rune(s)
for i, r := range runes {
isUpper := unicode.IsUpper(r)
if i+1 < len(runes) {
nextUpper = unicode.IsUpper(runes[i+1])
} else {
nextUpper = false
}
if i > 0 && isUpper {
// Add underscore before uppercase letter if:
// 1. Previous char was lowercase, OR
// 2. Next char is lowercase (end of acronym)
if !prevUpper || (!nextUpper && i+1 < len(runes)) {
result.WriteRune('_')
}
}
result.WriteRune(unicode.ToLower(r))
prevUpper = isUpper
}
return result.String()
}
// ToKebabCase converts snake_case or PascalCase/camelCase to kebab-case
// Examples: user_name → user-name, UserName → user-name
func ToKebabCase(s string) string {
// First convert to snake_case, then replace underscores with hyphens
snakeCase := ToSnakeCase(s)
return strings.ReplaceAll(snakeCase, "_", "-")
}
// Title capitalizes the first letter of each word
func Title(s string) string {
caser := cases.Title(language.English)
src := []byte(s)
dest := []byte(s)
_, _, _ = caser.Transform(dest, src, true)
return string(dest)
}
// Pluralize converts a singular word to plural
// Basic implementation with common English rules
func Pluralize(s string) string {
if s == "" {
return ""
}
// Special cases
irregular := map[string]string{
"person": "people",
"child": "children",
"tooth": "teeth",
"foot": "feet",
"man": "men",
"woman": "women",
"mouse": "mice",
"goose": "geese",
"ox": "oxen",
"datum": "data",
"medium": "media",
"analysis": "analyses",
"crisis": "crises",
"status": "statuses",
}
if plural, ok := irregular[strings.ToLower(s)]; ok {
return plural
}
// Already plural (ends in 's' but not 'ss' or 'us')
if strings.HasSuffix(s, "s") && !strings.HasSuffix(s, "ss") && !strings.HasSuffix(s, "us") {
return s
}
// Words ending in s, x, z, ch, sh
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "x") ||
strings.HasSuffix(s, "z") || strings.HasSuffix(s, "ch") ||
strings.HasSuffix(s, "sh") {
return s + "es"
}
// Words ending in consonant + y
if len(s) >= 2 && strings.HasSuffix(s, "y") {
prevChar := s[len(s)-2]
if !isVowel(prevChar) {
return s[:len(s)-1] + "ies"
}
}
// Words ending in f or fe
if strings.HasSuffix(s, "f") {
return s[:len(s)-1] + "ves"
}
if strings.HasSuffix(s, "fe") {
return s[:len(s)-2] + "ves"
}
// Words ending in consonant + o
if len(s) >= 2 && strings.HasSuffix(s, "o") {
prevChar := s[len(s)-2]
if !isVowel(prevChar) {
return s + "es"
}
}
// Default: add 's'
return s + "s"
}
// Singularize converts a plural word to singular
// Basic implementation with common English rules
func Singularize(s string) string {
if s == "" {
return ""
}
// Special cases
irregular := map[string]string{
"people": "person",
"children": "child",
"teeth": "tooth",
"feet": "foot",
"men": "man",
"women": "woman",
"mice": "mouse",
"geese": "goose",
"oxen": "ox",
"data": "datum",
"media": "medium",
"analyses": "analysis",
"crises": "crisis",
"statuses": "status",
}
if singular, ok := irregular[strings.ToLower(s)]; ok {
return singular
}
// Words ending in ies
if strings.HasSuffix(s, "ies") && len(s) > 3 {
return s[:len(s)-3] + "y"
}
// Words ending in ves
if strings.HasSuffix(s, "ves") {
return s[:len(s)-3] + "f"
}
// Words ending in ses, xes, zes, ches, shes
if strings.HasSuffix(s, "ses") || strings.HasSuffix(s, "xes") ||
strings.HasSuffix(s, "zes") || strings.HasSuffix(s, "ches") ||
strings.HasSuffix(s, "shes") {
return s[:len(s)-2]
}
// Words ending in s (not ss)
if strings.HasSuffix(s, "s") && !strings.HasSuffix(s, "ss") {
return s[:len(s)-1]
}
// Already singular
return s
}
// Trim trims whitespace from both ends
func Trim(s string) string {
return strings.TrimSpace(s)
}
// TrimPrefix removes the prefix from the string if present
func TrimPrefix(s, prefix string) string {
return strings.TrimPrefix(s, prefix)
}
// TrimSuffix removes the suffix from the string if present
func TrimSuffix(s, suffix string) string {
return strings.TrimSuffix(s, suffix)
}
// Replace replaces occurrences of old with new (n times, or all if n < 0)
func Replace(s, old, newstr string, n int) string {
return strings.Replace(s, old, newstr, n)
}
// StringContains checks if substr is within s
func StringContains(s, substr string) bool {
return strings.Contains(s, substr)
}
// HasPrefix checks if string starts with prefix
func HasPrefix(s, prefix string) bool {
return strings.HasPrefix(s, prefix)
}
// HasSuffix checks if string ends with suffix
func HasSuffix(s, suffix string) bool {
return strings.HasSuffix(s, suffix)
}
// Split splits string by separator
func Split(s, sep string) []string {
return strings.Split(s, sep)
}
// Join joins string slice with separator
func Join(parts []string, sep string) string {
return strings.Join(parts, sep)
}
// capitalize capitalizes the first letter and handles common acronyms
func capitalize(s string) string {
if s == "" {
return ""
}
upper := strings.ToUpper(s)
// Handle common acronyms
acronyms := map[string]bool{
"ID": true,
"UUID": true,
"GUID": true,
"URL": true,
"URI": true,
"HTTP": true,
"HTTPS": true,
"API": true,
"JSON": true,
"XML": true,
"SQL": true,
"HTML": true,
"CSS": true,
"RID": true,
}
if acronyms[upper] {
return upper
}
// Capitalize first letter
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}
// isVowel checks if a byte is a vowel
func isVowel(c byte) bool {
c = byte(unicode.ToLower(rune(c)))
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'
}

View File

@@ -0,0 +1,95 @@
package template
import "git.warky.dev/wdevs/relspecgo/pkg/models"
// TemplateData wraps the model data with additional context for template execution
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{}
}
// NewDatabaseData creates template data for database mode
func NewDatabaseData(db *models.Database, metadata map[string]interface{}) *TemplateData {
return &TemplateData{
Database: db,
ParentDatabase: db,
FlatColumns: db.ToFlatColumns(),
FlatTables: db.ToFlatTables(),
FlatConstraints: db.ToFlatConstraints(),
FlatRelationships: db.ToFlatRelationships(),
Summary: db.ToSummary(),
Metadata: metadata,
}
}
// NewSchemaData creates template data for schema mode
func NewSchemaData(schema *models.Schema, metadata map[string]interface{}) *TemplateData {
// Create a temporary database with just this schema for context
db := models.InitDatabase(schema.Name)
db.Schemas = []*models.Schema{schema}
return &TemplateData{
Schema: schema,
ParentDatabase: db,
FlatColumns: db.ToFlatColumns(),
FlatTables: db.ToFlatTables(),
FlatConstraints: db.ToFlatConstraints(),
FlatRelationships: db.ToFlatRelationships(),
Summary: db.ToSummary(),
Metadata: metadata,
}
}
// NewScriptData creates template data for script mode
func NewScriptData(script *models.Script, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData {
return &TemplateData{
Script: script,
ParentSchema: schema,
ParentDatabase: db,
Metadata: metadata,
}
}
// NewTableData creates template data for table mode
func NewTableData(table *models.Table, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData {
return &TemplateData{
Table: table,
ParentSchema: schema,
ParentDatabase: db,
Metadata: metadata,
}
}
// Name returns the primary name for the current template data (used for filename generation)
func (td *TemplateData) Name() string {
if td.Database != nil {
return td.Database.Name
}
if td.Schema != nil {
return td.Schema.Name
}
if td.Script != nil {
return td.Script.Name
}
if td.Table != nil {
return td.Table.Name
}
return "output"
}

View File

@@ -0,0 +1,38 @@
package template
import "git.warky.dev/wdevs/relspecgo/pkg/commontypes"
// SQLToGo converts SQL types to Go types
func SQLToGo(sqlType string, nullable bool) string {
return commontypes.SQLToGo(sqlType, nullable)
}
// SQLToTypeScript converts SQL types to TypeScript types
func SQLToTypeScript(sqlType string, nullable bool) string {
return commontypes.SQLToTypeScript(sqlType, nullable)
}
// SQLToJava converts SQL types to Java types
func SQLToJava(sqlType string, nullable bool) string {
return commontypes.SQLToJava(sqlType, nullable)
}
// SQLToPython converts SQL types to Python types
func SQLToPython(sqlType string) string {
return commontypes.SQLToPython(sqlType)
}
// SQLToRust converts SQL types to Rust types
func SQLToRust(sqlType string, nullable bool) string {
return commontypes.SQLToRust(sqlType, nullable)
}
// SQLToCSharp converts SQL types to C# types
func SQLToCSharp(sqlType string, nullable bool) string {
return commontypes.SQLToCSharp(sqlType, nullable)
}
// SQLToPhp converts SQL types to PHP types
func SQLToPhp(sqlType string, nullable bool) string {
return commontypes.SQLToPhp(sqlType, nullable)
}

View File

@@ -0,0 +1,283 @@
package template
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
// EntrypointMode defines how the template is executed
type EntrypointMode string
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"
)
// Writer implements the writers.Writer interface for template-based output
type Writer struct {
options *writers.WriterOptions
templatePath string
funcMap template.FuncMap
tmpl *template.Template
mode EntrypointMode
filenamePattern string
}
// NewWriter creates a new template writer with the given options
func NewWriter(options *writers.WriterOptions) (*Writer, error) {
w := &Writer{
options: options,
funcMap: BuildFuncMap(),
mode: DatabaseMode, // default mode
filenamePattern: "{{.Name}}.txt",
}
// Extract template path from metadata
if options.Metadata != nil {
if path, ok := options.Metadata["template_path"].(string); ok {
w.templatePath = path
}
if mode, ok := options.Metadata["mode"].(string); ok {
w.mode = EntrypointMode(mode)
}
if pattern, ok := options.Metadata["filename_pattern"].(string); ok {
w.filenamePattern = pattern
}
}
// Validate template path
if w.templatePath == "" {
return nil, NewTemplateLoadError("template path is required", nil)
}
// Load and parse template
if err := w.loadTemplate(); err != nil {
return nil, err
}
return w, nil
}
// WriteDatabase writes a database using the template
func (w *Writer) WriteDatabase(db *models.Database) error {
switch w.mode {
case DatabaseMode:
return w.executeDatabaseMode(db)
case SchemaMode:
return w.executeSchemaMode(db)
case ScriptMode:
return w.executeScriptMode(db)
case TableMode:
return w.executeTableMode(db)
default:
return fmt.Errorf("unknown entrypoint mode: %s", w.mode)
}
}
// WriteSchema writes a schema using the template
func (w *Writer) WriteSchema(schema *models.Schema) error {
// Create a temporary database with just this schema
db := models.InitDatabase(schema.Name)
db.Schemas = []*models.Schema{schema}
return w.WriteDatabase(db)
}
// WriteTable writes a single table using the template
func (w *Writer) WriteTable(table *models.Table) error {
// Create a temporary schema and database
schema := models.InitSchema(table.Schema)
schema.Tables = []*models.Table{table}
db := models.InitDatabase(schema.Name)
db.Schemas = []*models.Schema{schema}
return w.WriteDatabase(db)
}
// executeDatabaseMode executes the template once for the entire database
func (w *Writer) executeDatabaseMode(db *models.Database) error {
data := NewDatabaseData(db, w.options.Metadata)
output, err := w.executeTemplate(data)
if err != nil {
return err
}
return w.writeOutput(output, w.options.OutputPath)
}
// executeSchemaMode executes the template once per schema
func (w *Writer) executeSchemaMode(db *models.Database) error {
for _, schema := range db.Schemas {
data := NewSchemaData(schema, w.options.Metadata)
output, err := w.executeTemplate(data)
if err != nil {
return fmt.Errorf("failed to execute template for schema %s: %w", schema.Name, err)
}
filename, err := w.generateFilename(data)
if err != nil {
return fmt.Errorf("failed to generate filename for schema %s: %w", schema.Name, err)
}
if err := w.writeOutput(output, filename); err != nil {
return fmt.Errorf("failed to write output for schema %s: %w", schema.Name, err)
}
}
return nil
}
// executeScriptMode executes the template once per script
func (w *Writer) executeScriptMode(db *models.Database) error {
for _, schema := range db.Schemas {
for _, script := range schema.Scripts {
data := NewScriptData(script, schema, db, w.options.Metadata)
output, err := w.executeTemplate(data)
if err != nil {
return fmt.Errorf("failed to execute template for script %s: %w", script.Name, err)
}
filename, err := w.generateFilename(data)
if err != nil {
return fmt.Errorf("failed to generate filename for script %s: %w", script.Name, err)
}
if err := w.writeOutput(output, filename); err != nil {
return fmt.Errorf("failed to write output for script %s: %w", script.Name, err)
}
}
}
return nil
}
// executeTableMode executes the template once per table
func (w *Writer) executeTableMode(db *models.Database) error {
for _, schema := range db.Schemas {
for _, table := range schema.Tables {
data := NewTableData(table, schema, db, w.options.Metadata)
output, err := w.executeTemplate(data)
if err != nil {
return fmt.Errorf("failed to execute template for table %s.%s: %w", schema.Name, table.Name, err)
}
filename, err := w.generateFilename(data)
if err != nil {
return fmt.Errorf("failed to generate filename for table %s.%s: %w", schema.Name, table.Name, err)
}
if err := w.writeOutput(output, filename); err != nil {
return fmt.Errorf("failed to write output for table %s.%s: %w", schema.Name, table.Name, err)
}
}
}
return nil
}
// loadTemplate loads and parses the template file
func (w *Writer) loadTemplate() error {
// Read template file
content, err := os.ReadFile(w.templatePath)
if err != nil {
return NewTemplateLoadError(fmt.Sprintf("failed to read template file: %s", w.templatePath), err)
}
// Parse template with function map
tmpl, err := template.New(filepath.Base(w.templatePath)).Funcs(w.funcMap).Parse(string(content))
if err != nil {
return NewTemplateParseError(fmt.Sprintf("failed to parse template: %s", w.templatePath), err)
}
w.tmpl = tmpl
return nil
}
// executeTemplate executes the template with the given data
func (w *Writer) executeTemplate(data *TemplateData) (string, error) {
var buf bytes.Buffer
if err := w.tmpl.Execute(&buf, data); err != nil {
return "", NewTemplateExecuteError("failed to execute template", err)
}
return buf.String(), nil
}
// generateFilename generates a filename from the filename pattern
func (w *Writer) generateFilename(data *TemplateData) (string, error) {
// Parse filename pattern as a template
tmpl, err := template.New("filename").Funcs(w.funcMap).Parse(w.filenamePattern)
if err != nil {
return "", fmt.Errorf("invalid filename pattern: %w", err)
}
// Execute filename template
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("failed to generate filename: %w", err)
}
filename := buf.String()
// If output path is a directory, join with generated filename
if w.options.OutputPath != "" {
// Check if output path is a directory
info, err := os.Stat(w.options.OutputPath)
if err == nil && info.IsDir() {
filename = filepath.Join(w.options.OutputPath, filename)
} else {
// If it doesn't exist, check if it looks like a directory (ends with /)
if strings.HasSuffix(w.options.OutputPath, string(filepath.Separator)) {
filename = filepath.Join(w.options.OutputPath, filename)
} else {
// Use output path as base directory
dir := filepath.Dir(w.options.OutputPath)
if dir != "." {
filename = filepath.Join(dir, filename)
}
}
}
}
return filename, nil
}
// writeOutput writes the output to a file or stdout
func (w *Writer) writeOutput(content string, outputPath string) error {
// If output path is empty, write to stdout
if outputPath == "" {
fmt.Print(content)
return nil
}
// Ensure directory exists
dir := filepath.Dir(outputPath)
if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
// Write to file
if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
}
return nil
}