feat(templ): ✨ added templ to command line that reads go template and outputs code
This commit is contained in:
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