From 382129d2bb3015da4ac93ee82ebcbd6e671e958c Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Mon, 1 Sep 2025 13:57:09 -0600 Subject: [PATCH] working interpreter for template files --- .idea/copilotDiffState.xml | 42 ---- cmd/cli/commands.go | 73 +++++- examples/lang/example.masonry | 1 - interpreter/template_interpreter.go | 256 ++++++++++++++++++++ lang_templates/golang/basic_go_server.tmpl | 91 +++++++ lang_templates/proto/application.proto.tmpl | 105 ++++++++ 6 files changed, 522 insertions(+), 46 deletions(-) delete mode 100644 .idea/copilotDiffState.xml create mode 100644 interpreter/template_interpreter.go create mode 100644 lang_templates/golang/basic_go_server.tmpl create mode 100644 lang_templates/proto/application.proto.tmpl diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml deleted file mode 100644 index 986686f..0000000 --- a/.idea/copilotDiffState.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/cmd/cli/commands.go b/cmd/cli/commands.go index a3a3eea..ae3d10d 100644 --- a/cmd/cli/commands.go +++ b/cmd/cli/commands.go @@ -5,9 +5,6 @@ import ( "embed" _ "embed" "fmt" - "github.com/urfave/cli/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" vue_gen "masonry/vue-gen" "os" "os/exec" @@ -16,6 +13,10 @@ import ( "strings" "text/template" + "github.com/urfave/cli/v2" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "github.com/alecthomas/participle/v2" "masonry/interpreter" @@ -572,3 +573,69 @@ func serveCmd() *cli.Command { }, } } + +func templateCmd() *cli.Command { + return &cli.Command{ + Name: "template", + Aliases: []string{"tmpl"}, + Usage: "Generate code from templates using Masonry DSL", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "templates", + Usage: "Path to template directory", + Value: "./lang_templates", + Aliases: []string{"t"}, + }, + &cli.StringFlag{ + Name: "output", + Usage: "Output destination directory", + Value: "./output", + Aliases: []string{"o"}, + }, + &cli.StringFlag{ + Name: "input", + Usage: "Input Masonry file path", + Required: true, + Aliases: []string{"i"}, + }, + }, + Action: func(c *cli.Context) error { + templateDir := c.String("templates") + outputDir := c.String("output") + inputFile := c.String("input") + + fmt.Printf("Processing templates from: %s\n", templateDir) + fmt.Printf("Input file: %s\n", inputFile) + fmt.Printf("Output directory: %s\n", outputDir) + + // Read the Masonry file + content, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("error reading Masonry file: %w", err) + } + + // Parse the Masonry file + parser, err := participle.Build[lang.AST]( + participle.Unquote("String"), + ) + if err != nil { + return fmt.Errorf("error building parser: %w", err) + } + + ast, err := parser.ParseString("", string(content)) + if err != nil { + return fmt.Errorf("error parsing Masonry file: %w", err) + } + + // Create template interpreter and process templates + templateInterpreter := interpreter.NewTemplateInterpreter() + err = templateInterpreter.ProcessTemplates(*ast, templateDir, outputDir) + if err != nil { + return fmt.Errorf("error processing templates: %w", err) + } + + fmt.Println("Template processing completed successfully!") + return nil + }, + } +} diff --git a/examples/lang/example.masonry b/examples/lang/example.masonry index 15a1299..f93120e 100644 --- a/examples/lang/example.masonry +++ b/examples/lang/example.masonry @@ -1,5 +1,4 @@ // Enhanced Masonry DSL example demonstrating simplified unified structure -// This shows how containers, tabs, panels, modals, and master-detail are now unified as sections // Server configuration server MyApp { diff --git a/interpreter/template_interpreter.go b/interpreter/template_interpreter.go new file mode 100644 index 0000000..81b1202 --- /dev/null +++ b/interpreter/template_interpreter.go @@ -0,0 +1,256 @@ +package interpreter + +import ( + "bytes" + "fmt" + "masonry/lang" + "os" + "path/filepath" + "regexp" + "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 with helper functions + tmpl := template.Must(template.New("rootTemplate").Funcs(template.FuncMap{ + "registry": func() *TemplateRegistry { return ti.registry }, + "executeTemplate": func(name string, data interface{}) (string, error) { + if tmpl, exists := ti.registry.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 := ti.registry.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 = strings.Title(name) + return strings.ReplaceAll(name, " ", "") + }, + "getHost": func(settings []lang.ServerSetting) string { + for _, s := range settings { + if s.Host != nil { + return *s.Host + } + } + return "localhost" + }, + "getPort": func(settings []lang.ServerSetting) int { + for _, s := range settings { + if s.Port != nil { + return *s.Port + } + } + return 8080 + }, + "slice": func() []interface{} { + return []interface{}{} + }, + "append": func(slice []interface{}, item interface{}) []interface{} { + return append(slice, item) + }, + }).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 = strings.Title(name) + return strings.ReplaceAll(name, " ", "") + }, + "getHost": func(settings []lang.ServerSetting) string { + for _, s := range settings { + if s.Host != nil { + return *s.Host + } + } + return "localhost" + }, + "getPort": func(settings []lang.ServerSetting) int { + for _, s := range settings { + if s.Port != nil { + return *s.Port + } + } + return 8080 + }, + "slice": func() []interface{} { + return []interface{}{} + }, + "append": func(slice []interface{}, item interface{}) []interface{} { + return append(slice, item) + }, + } + return tr +} + +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 + }) +} diff --git a/lang_templates/golang/basic_go_server.tmpl b/lang_templates/golang/basic_go_server.tmpl new file mode 100644 index 0000000..ae01dc6 --- /dev/null +++ b/lang_templates/golang/basic_go_server.tmpl @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "net/http" + "encoding/json" + "log" + "github.com/gorilla/mux" +) + +{{- range .AST.Definitions }} +{{- if .Server }} +// Server configuration +const ( + HOST = "{{ .Server.Settings | getHost }}" + PORT = {{ .Server.Settings | getPort }} +) +{{- end }} +{{- end }} + +{{- range .AST.Definitions }} +{{- if .Entity }} +// {{ .Entity.Name }} represents {{ .Entity.Description }} +type {{ .Entity.Name }} struct { +{{- range .Entity.Fields }} + {{ .Name | title }} {{ .Type | goType }} `json:"{{ .Name }}"{{ if .Required }} validate:"required"{{ end }}` +{{- end }} +} +{{- end }} +{{- end }} + +{{- $endpoints := slice }} +{{- range .AST.Definitions }} +{{- if .Endpoint }} + {{- $endpoints = append $endpoints . }} +{{- end }} +{{- end }} + +{{- range $endpoints }} +// {{ .Endpoint.Description }} +func {{ .Endpoint.Path | pathToHandlerName }}{{ .Endpoint.Method | title }}Handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + {{- if .Endpoint.Auth }} + // TODO: Add authentication middleware + {{- end }} + + {{- range .Endpoint.Params }} + {{- if eq .Source "path" }} + vars := mux.Vars(r) + {{ .Name }} := vars["{{ .Name }}"] + {{- else if eq .Source "query" }} + {{ .Name }} := r.URL.Query().Get("{{ .Name }}") + {{- else if eq .Source "body" }} + var {{ .Name }} {{ .Type | goType }} + if err := json.NewDecoder(r.Body).Decode(&{{ .Name }}); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + {{- end }} + {{- end }} + + {{- if .Endpoint.CustomLogic }} + // Custom logic: {{ .Endpoint.CustomLogic }} + {{- else }} + // TODO: Implement {{ .Endpoint.Method }} {{ .Endpoint.Path }} logic + {{- end }} + + {{- if .Endpoint.Response }} + {{- if eq .Endpoint.Response.Type "list" }} + response := []{{ .Endpoint.Entity }}{} + {{- else }} + response := {{ .Endpoint.Entity }}{} + {{- end }} + json.NewEncoder(w).Encode(response) + {{- else }} + w.WriteHeader(http.StatusOK) + {{- end }} +} +{{- end }} + +func main() { + router := mux.NewRouter() + + {{- range $endpoints }} + router.HandleFunc("{{ .Endpoint.Path }}", {{ .Endpoint.Path | pathToHandlerName }}{{ .Endpoint.Method | title }}Handler).Methods("{{ .Endpoint.Method }}") + {{- end }} + + fmt.Printf("Server starting on %s:%d\n", HOST, PORT) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", HOST, PORT), router)) +} \ No newline at end of file diff --git a/lang_templates/proto/application.proto.tmpl b/lang_templates/proto/application.proto.tmpl new file mode 100644 index 0000000..56ec33d --- /dev/null +++ b/lang_templates/proto/application.proto.tmpl @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +syntax = "proto3"; + +package {{ .AppName }}; + +import "gorm/options/gorm.proto"; +//import "gorm/types/types.proto"; +import "google/api/annotations.proto"; + +option go_package = "./;pb"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Your API Title" + version: "v1.0" + description: "Your API description" + } + host: "localhost:8080" // Set the server host +}; + +service {{ .AppNameCaps }} { + option (gorm.server).autogen = true; + // Add your service methods here + + rpc CreateProduct (CreateProductRequest) returns (CreateProductResponse) { + option (google.api.http) = { + post: "/v1/Product" + body: "*" + }; + } + + rpc ReadProduct (ReadProductRequest) returns (ReadProductResponse) { + option (google.api.http) = { + get: "/v1/Product/{id}" + }; + } + + rpc ListProducts (ListProductsRequest) returns (ListProductsResponse) { + option (google.api.http) = { + get: "/v1/Product" + }; + } + + rpc UpdateProduct (UpdateProductRequest) returns (UpdateProductResponse) { + option (google.api.http) = { + put: "/v1/Product" + body: "*" + }; + } + + rpc DeleteProduct (DeleteProductRequest) returns (DeleteProductResponse) { + option (gorm.method).object_type = "Product"; + option (google.api.http) = { + delete: "/v1/Product/{id}" + }; + } +} + +message Create{{ .ObjName }}Request { + {{ .ObjName }} payload = 1; +} + +message Create{{ .ObjName }}Response { + {{ .ObjName }} result = 1; +} + +message Read{{ .ObjName }}Request { + uint64 id = 1; +} + +message Read{{ .ObjName }}Response { + {{ .ObjName }} result = 1; +} + +message List{{ .ObjName }}sRequest {} + +message List{{ .ObjName }}sResponse { + repeated {{ .ObjName }} results = 1; +} + +message Update{{ .ObjName }}Request { + {{ .ObjName }} payload = 1; +} + +message Update{{ .ObjName }}Response { + {{ .ObjName }} result = 1; +} + +message Delete{{ .ObjName }}Request { + uint64 id = 1; +} + +message Delete{{ .ObjName }}Response {} + +message {{ .ObjName }} { + option (gorm.opts).ormable = true; + uint64 id = 1; + // add object fields here +} + + + + + +