working interpreter for template files

This commit is contained in:
2025-09-01 13:57:09 -06:00
parent 23e84c263d
commit 382129d2bb
6 changed files with 522 additions and 46 deletions

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,6 @@ import (
"embed" "embed"
_ "embed" _ "embed"
"fmt" "fmt"
"github.com/urfave/cli/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
vue_gen "masonry/vue-gen" vue_gen "masonry/vue-gen"
"os" "os"
"os/exec" "os/exec"
@ -16,6 +13,10 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/urfave/cli/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2"
"masonry/interpreter" "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
},
}
}

View File

@ -1,5 +1,4 @@
// Enhanced Masonry DSL example demonstrating simplified unified structure // 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 configuration
server MyApp { server MyApp {

View File

@ -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
})
}

View File

@ -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))
}

View File

@ -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
}