Files
masonry/interpreter/template_interpreter.go
Mason Payne 0bccd28134 refactor template functions to remove duplication
update readme TODOs
update plan for multi-file outputs
2025-09-04 23:53:16 -06:00

266 lines
6.9 KiB
Go

package interpreter
import (
"bytes"
"fmt"
"masonry/lang"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"text/template"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// TemplateInterpreter converts Masonry AST using template files
type TemplateInterpreter struct {
registry *TemplateRegistry
}
// NewTemplateInterpreter creates a new template interpreter
func NewTemplateInterpreter() *TemplateInterpreter {
return &TemplateInterpreter{
registry: NewTemplateRegistry(),
}
}
// InterpretFromFile parses a Masonry file and applies a template file
func (ti *TemplateInterpreter) InterpretFromFile(masonryFile, templateFile string) (string, error) {
// Read the Masonry file
masonryInput, err := os.ReadFile(masonryFile)
if err != nil {
return "", fmt.Errorf("error reading Masonry file: %w", err)
}
// Read the template file
tmplText, err := os.ReadFile(templateFile)
if err != nil {
return "", fmt.Errorf("error reading template file: %w", err)
}
return ti.Interpret(string(masonryInput), string(tmplText))
}
// InterpretFromDirectory parses a Masonry file and applies templates from a directory
func (ti *TemplateInterpreter) InterpretFromDirectory(masonryFile, templateDir, rootTemplate string) (string, error) {
// Load all templates from the directory
err := ti.registry.LoadFromDirectory(templateDir)
if err != nil {
return "", fmt.Errorf("error loading templates from directory: %w", err)
}
// Read the Masonry file
masonryInput, err := os.ReadFile(masonryFile)
if err != nil {
return "", fmt.Errorf("error reading Masonry file: %w", err)
}
// Get the root template content
rootTemplatePath := filepath.Join(templateDir, rootTemplate)
tmplText, err := os.ReadFile(rootTemplatePath)
if err != nil {
return "", fmt.Errorf("error reading root template file: %w", err)
}
return ti.Interpret(string(masonryInput), string(tmplText))
}
func (ti *TemplateInterpreter) Interpret(masonryInput string, tmplText string) (string, error) {
ast, err := lang.ParseInput(masonryInput)
if err != nil {
return "", fmt.Errorf("error parsing Masonry input: %w", err)
}
// Create template using the unified FuncMap from the registry
tmpl := template.Must(template.New("rootTemplate").Funcs(ti.registry.GetFuncMap()).Parse(tmplText))
data := struct {
AST lang.AST
Registry *TemplateRegistry
}{
AST: ast,
Registry: ti.registry,
}
var buf bytes.Buffer
// Execute template
err = tmpl.Execute(&buf, data)
if err != nil {
return "", fmt.Errorf("error executing template: %w", err)
}
return buf.String(), nil
}
type TemplateRegistry struct {
templates map[string]*template.Template
funcMap template.FuncMap
}
func NewTemplateRegistry() *TemplateRegistry {
tr := &TemplateRegistry{
templates: make(map[string]*template.Template),
}
// Create funcMap with helper functions that will be available in all templates
tr.funcMap = template.FuncMap{
"executeTemplate": func(name string, data interface{}) (string, error) {
if tmpl, exists := tr.templates[name]; exists {
var buf strings.Builder
err := tmpl.Execute(&buf, data)
return buf.String(), err
}
return "", fmt.Errorf("template %s not found", name)
},
"hasTemplate": func(name string) bool {
_, exists := tr.templates[name]
return exists
},
"title": cases.Title(language.English).String,
"goType": func(t string) string {
typeMap := map[string]string{
"string": "string",
"int": "int",
"uuid": "string",
"boolean": "bool",
"timestamp": "time.Time",
"text": "string",
"object": "interface{}",
}
if goType, ok := typeMap[t]; ok {
return goType
}
return "interface{}"
},
"pathToHandlerName": func(path string) string {
// Convert "/users/{id}" to "Users"
re := regexp.MustCompile(`[^a-zA-Z0-9]+`)
name := re.ReplaceAllString(path, " ")
name = strings.TrimSpace(name)
name = cases.Title(language.English).String(name)
return strings.ReplaceAll(name, " ", "")
},
"getHost": func(settings []lang.ServerSetting) string {
for _, s := range settings {
if s.Host != nil {
if s.Host.Literal != nil {
return "\"" + *s.Host.Literal + "\""
}
return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Host.EnvVar.Name, func() string {
if s.Host.EnvVar.Default != nil {
return *s.Host.EnvVar.Default
}
return "localhost"
}())
}
}
return "localhost"
},
"getPort": func(settings []lang.ServerSetting) string {
for _, s := range settings {
if s.Port != nil {
if s.Port.Literal != nil {
return strconv.Itoa(*s.Port.Literal)
}
return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Port.EnvVar.Name, func() string {
if s.Port.EnvVar.Default != nil {
return *s.Port.EnvVar.Default
}
return "8080"
}())
}
}
return "8080"
},
"getServerHostPort": func(settings []lang.ServerSetting) string {
host := "localhost"
port := 8080
for _, s := range settings {
if s.Host != nil {
if s.Host.Literal != nil {
host = *s.Host.Literal
}
if s.Host.EnvVar != nil && s.Host.EnvVar.Default != nil {
host = *s.Host.EnvVar.Default
}
// If it's an env var, keep the default
}
if s.Port != nil {
if s.Port.Literal != nil {
port = *s.Port.Literal
}
if s.Port.EnvVar != nil && s.Port.EnvVar.Default != nil {
if p, err := strconv.Atoi(*s.Port.EnvVar.Default); err == nil {
port = p
}
}
// If it's an env var, keep the default
}
}
return fmt.Sprintf("%s:%d", host, port)
},
"slice": func() []interface{} {
return []interface{}{}
},
"append": func(slice []interface{}, item interface{}) []interface{} {
return append(slice, item)
},
"add": func(a, b int) int {
return a + b
},
"derefString": func(s *string) string {
if s != nil {
return *s
}
return ""
},
"derefInt": func(i *int) int {
if i != nil {
return *i
}
return 0
},
}
return tr
}
func (tr *TemplateRegistry) GetFuncMap() template.FuncMap {
// Add the registry function to the existing funcMap
funcMap := make(template.FuncMap)
for k, v := range tr.funcMap {
funcMap[k] = v
}
funcMap["registry"] = func() *TemplateRegistry { return tr }
return funcMap
}
func (tr *TemplateRegistry) Register(name, content string) error {
tmpl, err := template.New(name).Funcs(tr.funcMap).Parse(content)
if err != nil {
return err
}
tr.templates[name] = tmpl
return nil
}
func (tr *TemplateRegistry) LoadFromDirectory(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".tmpl") {
content, err := os.ReadFile(path)
if err != nil {
return err
}
name := strings.TrimSuffix(filepath.Base(path), ".tmpl")
return tr.Register(name, string(content))
}
return nil
})
}