- 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.
310 lines
8.9 KiB
Go
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
|
|
}
|