feat(templ): ✨ added templ to command line that reads go template and outputs code
This commit is contained in:
276
pkg/writers/template/README.md
Normal file
276
pkg/writers/template/README.md
Normal 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
|
||||
50
pkg/writers/template/errors.go
Normal file
50
pkg/writers/template/errors.go
Normal 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,
|
||||
}
|
||||
}
|
||||
144
pkg/writers/template/filters.go
Normal file
144
pkg/writers/template/filters.go
Normal 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
|
||||
}
|
||||
157
pkg/writers/template/formatters.go
Normal file
157
pkg/writers/template/formatters.go
Normal 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
|
||||
}
|
||||
171
pkg/writers/template/funcmap.go
Normal file
171
pkg/writers/template/funcmap.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
282
pkg/writers/template/loop_helpers.go
Normal file
282
pkg/writers/template/loop_helpers.go
Normal 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)
|
||||
}
|
||||
288
pkg/writers/template/safe_access.go
Normal file
288
pkg/writers/template/safe_access.go
Normal 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
|
||||
}
|
||||
316
pkg/writers/template/string_helpers.go
Normal file
316
pkg/writers/template/string_helpers.go
Normal 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'
|
||||
}
|
||||
95
pkg/writers/template/template_data.go
Normal file
95
pkg/writers/template/template_data.go
Normal 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"
|
||||
}
|
||||
38
pkg/writers/template/type_mappers.go
Normal file
38
pkg/writers/template/type_mappers.go
Normal 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)
|
||||
}
|
||||
283
pkg/writers/template/writer.go
Normal file
283
pkg/writers/template/writer.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user