Files
relspecgo/pkg/writers/template/writer.go
Hein 5d3c86119e
Some checks failed
CI / Test (1.24) (push) Successful in -27m28s
CI / Test (1.25) (push) Successful in -27m30s
CI / Build (push) Failing after -28m36s
Integration Tests / Integration Tests (push) Failing after -28m8s
CI / Lint (push) Successful in -27m54s
feat(domains): add domain support for DrawDB integration
- Introduce Domain and DomainTable models for logical grouping of tables.
- Implement export and import functionality for domains in DrawDB format.
- Update template execution modes to include domain processing.
- Enhance documentation for domain features and usage.
2026-01-04 15:49:47 +02:00

310 lines
8.9 KiB
Go

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"
// DomainMode executes the template once per domain (multi-file output)
DomainMode EntrypointMode = "domain"
// 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 DomainMode:
return w.executeDomainMode(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
}
// executeDomainMode executes the template once per domain
func (w *Writer) executeDomainMode(db *models.Database) error {
for _, domain := range db.Domains {
data := NewDomainData(domain, db, w.options.Metadata)
output, err := w.executeTemplate(data)
if err != nil {
return fmt.Errorf("failed to execute template for domain %s: %w", domain.Name, err)
}
filename, err := w.generateFilename(data)
if err != nil {
return fmt.Errorf("failed to generate filename for domain %s: %w", domain.Name, err)
}
if err := w.writeOutput(output, filename); err != nil {
return fmt.Errorf("failed to write output for domain %s: %w", domain.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
}