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 }