From d36e1bfd86547a88aa083aa2d2392b5535944839 Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Mon, 25 Aug 2025 00:50:55 -0600 Subject: [PATCH] add html and server interpreters these are basic and lack most features. the server seems to work the best. the html on the other hand is really rough and doesn't seem to work yet. but it does build the pages and they have all the shapes and sections we wanted. More work to come. :) --- .gitignore | 1 - cmd/cli/cli.go | 1 + cmd/cli/commands.go | 108 +++++++++ interpreter/html_interpreter.go | 191 ++++++++++++++- interpreter/server_interpreter.go | 386 ++++++++++++++++++++++++++++++ 5 files changed, 681 insertions(+), 6 deletions(-) create mode 100644 interpreter/server_interpreter.go diff --git a/.gitignore b/.gitignore index 533b18e..41b6826 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -/html-output/ /masonry.exe diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 6c7f7a4..5319e42 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -14,6 +14,7 @@ func main() { tailwindCmd(), setupCmd(), vueGenCmd(), + serveCmd(), // New command for server interpreter } app := &cli.App{ diff --git a/cmd/cli/commands.go b/cmd/cli/commands.go index 30861c9..a3a3eea 100644 --- a/cmd/cli/commands.go +++ b/cmd/cli/commands.go @@ -464,3 +464,111 @@ func generateHTML(inputFile, outputDir string) error { fmt.Printf("Successfully generated %d HTML file(s)\n", len(htmlFiles)) return nil } + +func serveCmd() *cli.Command { + return &cli.Command{ + Name: "serve", + Usage: "Generate and run a simple HTTP server from a Masonry file", + Description: "This command parses a Masonry file and generates a simple Go HTTP server with in-memory database.", + Category: "development", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Usage: "Path to the Masonry file to interpret", + Required: true, + Aliases: []string{"f"}, + }, + &cli.StringFlag{ + Name: "output", + Usage: "Output file for the generated server code", + Value: "server.go", + Aliases: []string{"o"}, + }, + &cli.BoolFlag{ + Name: "run", + Usage: "Run the server after generating it", + Value: false, + Aliases: []string{"r"}, + }, + }, + Action: func(c *cli.Context) error { + masonryFile := c.String("file") + outputFile := c.String("output") + shouldRun := c.Bool("run") + + fmt.Printf("Parsing Masonry file: %s\n", masonryFile) + + // Read the Masonry file + content, err := os.ReadFile(masonryFile) + 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) + } + + // Generate server code using the server interpreter + serverInterpreter := interpreter.NewServerInterpreter() + serverCode, err := serverInterpreter.Interpret(*ast) + if err != nil { + return fmt.Errorf("error interpreting Masonry file: %w", err) + } + + // Write the generated server code to the output file + err = os.WriteFile(outputFile, []byte(serverCode), 0644) + if err != nil { + return fmt.Errorf("error writing server code to file: %w", err) + } + + fmt.Printf("Server code generated successfully: %s\n", outputFile) + + if shouldRun { + fmt.Println("Installing dependencies...") + + // Initialize go module if it doesn't exist + if _, err := os.Stat("go.mod"); os.IsNotExist(err) { + cmd := exec.Command("go", "mod", "init", "masonry-server") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("error initializing go module: %w", err) + } + } + + // Install required dependencies + dependencies := []string{ + "github.com/google/uuid", + "github.com/gorilla/mux", + } + + for _, dep := range dependencies { + fmt.Printf("Installing %s...\n", dep) + cmd := exec.Command("go", "get", dep) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("error installing dependency %s: %w", dep, err) + } + } + + fmt.Printf("Running server from %s...\n", outputFile) + cmd := exec.Command("go", "run", outputFile) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } + + return nil + }, + } +} diff --git a/interpreter/html_interpreter.go b/interpreter/html_interpreter.go index c35afdc..6d6275c 100644 --- a/interpreter/html_interpreter.go +++ b/interpreter/html_interpreter.go @@ -10,6 +10,7 @@ import ( type HTMLInterpreter struct { entities map[string]*lang.Entity pages map[string]*lang.Page + server *lang.Server } // NewHTMLInterpreter creates a new HTML interpreter @@ -40,7 +41,7 @@ func (hi *HTMLInterpreter) escapeHTML(s string) string { // GenerateHTML converts a Masonry AST to HTML output func (hi *HTMLInterpreter) GenerateHTML(ast *lang.AST) (map[string]string, error) { - // First pass: collect entities and pages + // First pass: collect entities, pages, and server config for _, def := range ast.Definitions { if def.Entity != nil { hi.entities[def.Entity.Name] = def.Entity @@ -48,6 +49,9 @@ func (hi *HTMLInterpreter) GenerateHTML(ast *lang.AST) (map[string]string, error if def.Page != nil { hi.pages[def.Page.Name] = def.Page } + if def.Server != nil { + hi.server = def.Server + } } // Second pass: generate HTML for each page @@ -135,6 +139,86 @@ func (hi *HTMLInterpreter) generatePageHTML(page *lang.Page) (string, error) { // JavaScript for interactivity html.WriteString(" \n") html.WriteString("\n") diff --git a/interpreter/server_interpreter.go b/interpreter/server_interpreter.go new file mode 100644 index 0000000..cb6a7cb --- /dev/null +++ b/interpreter/server_interpreter.go @@ -0,0 +1,386 @@ +package interpreter + +import ( + "fmt" + "go/format" + "masonry/lang" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// ServerInterpreter converts Masonry AST to a simple Go HTTP server +type ServerInterpreter struct { + entities map[string]*lang.Entity + endpoints map[string]*lang.Endpoint + server *lang.Server +} + +// NewServerInterpreter creates a new server interpreter +func NewServerInterpreter() *ServerInterpreter { + return &ServerInterpreter{ + entities: make(map[string]*lang.Entity), + endpoints: make(map[string]*lang.Endpoint), + } +} + +// Interpret processes the AST and generates server code +func (si *ServerInterpreter) Interpret(ast lang.AST) (string, error) { + // First pass: collect all definitions + for _, def := range ast.Definitions { + if def.Server != nil { + si.server = def.Server + } + if def.Entity != nil { + si.entities[def.Entity.Name] = def.Entity + } + if def.Endpoint != nil { + key := fmt.Sprintf("%s_%s", def.Endpoint.Method, strings.ReplaceAll(def.Endpoint.Path, "/", "_")) + si.endpoints[key] = def.Endpoint + } + } + + // Generate the server code + rawCode, err := si.generateServer() + if err != nil { + return "", err + } + + // Format the code to remove unused imports and fix formatting + formattedCode, err := format.Source([]byte(rawCode)) + if err != nil { + // If formatting fails, return the raw code with a warning comment + return "// Warning: Code formatting failed, but code should still be functional\n" + rawCode, nil + } + + return string(formattedCode), nil +} + +// generateServer creates the complete server code +func (si *ServerInterpreter) generateServer() (string, error) { + var code strings.Builder + + // Package and imports - only include what's actually used + code.WriteString(`package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "sync" + "time" + "github.com/google/uuid" + "github.com/gorilla/mux" +) + +`) + + // Generate entity structs + code.WriteString("// Entity definitions\n") + for _, entity := range si.entities { + code.WriteString(si.generateEntityStruct(entity)) + code.WriteString("\n") + } + + // Generate in-memory database + code.WriteString(si.generateInMemoryDB()) + + // Generate handlers + code.WriteString("// HTTP Handlers\n") + for _, entity := range si.entities { + code.WriteString(si.generateEntityHandlers(entity)) + } + + // Generate custom endpoint handlers + for _, endpoint := range si.endpoints { + if endpoint.Entity == nil { + code.WriteString(si.generateCustomEndpointHandler(endpoint)) + } + } + + // Generate main function + code.WriteString(si.generateMainFunction()) + + return code.String(), nil +} + +// generateEntityStruct creates a Go struct for an entity +func (si *ServerInterpreter) generateEntityStruct(entity *lang.Entity) string { + var code strings.Builder + + if entity.Description != nil { + code.WriteString(fmt.Sprintf("// %s - %s\n", entity.Name, *entity.Description)) + } + + code.WriteString(fmt.Sprintf("type %s struct {\n", entity.Name)) + + // Always add ID field + code.WriteString("\tID string `json:\"id\"`\n") + code.WriteString("\tCreatedAt time.Time `json:\"created_at\"`\n") + code.WriteString("\tUpdatedAt time.Time `json:\"updated_at\"`\n") + + for _, field := range entity.Fields { + goType := si.convertToGoType(field.Type) + jsonTag := strings.ToLower(field.Name) + + if !field.Required { + goType = "*" + goType + } + + code.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n", + cases.Title(language.English).String(field.Name), goType, jsonTag)) + } + + code.WriteString("}\n") + return code.String() +} + +// convertToGoType maps Masonry types to Go types +func (si *ServerInterpreter) convertToGoType(masonryType string) string { + switch masonryType { + case "string", "text", "email", "url": + return "string" + case "int", "number": + return "int" + case "float": + return "float64" + case "boolean": + return "bool" + case "uuid": + return "string" + case "timestamp": + return "time.Time" + default: + return "string" // default fallback + } +} + +// generateInMemoryDB creates the in-memory database structure +func (si *ServerInterpreter) generateInMemoryDB() string { + var code strings.Builder + + code.WriteString("// In-memory database\n") + code.WriteString("type InMemoryDB struct {\n") + code.WriteString("\tmu sync.RWMutex\n") + + for entityName := range si.entities { + code.WriteString(fmt.Sprintf("\t%s map[string]*%s\n", + strings.ToLower(entityName)+"s", entityName)) + } + + code.WriteString("}\n\n") + + code.WriteString("var db = &InMemoryDB{\n") + for entityName := range si.entities { + code.WriteString(fmt.Sprintf("\t%s: make(map[string]*%s),\n", + strings.ToLower(entityName)+"s", entityName)) + } + code.WriteString("}\n\n") + + return code.String() +} + +// generateEntityHandlers creates CRUD handlers for an entity +func (si *ServerInterpreter) generateEntityHandlers(entity *lang.Entity) string { + var code strings.Builder + entityName := entity.Name + entityLower := strings.ToLower(entityName) + entityPlural := entityLower + "s" + + // List handler + code.WriteString(fmt.Sprintf(`func list%sHandler(w http.ResponseWriter, r *http.Request) { + db.mu.RLock() + defer db.mu.RUnlock() + + var items []*%s + for _, item := range db.%s { + items = append(items, item) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(items) +} + +`, entityName, entityName, entityPlural)) + + // Get by ID handler + code.WriteString(fmt.Sprintf(`func get%sHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + db.mu.RLock() + item, exists := db.%s[id] + db.mu.RUnlock() + + if !exists { + http.Error(w, "%s not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(item) +} + +`, entityName, entityPlural, entityName)) + + // Create handler + code.WriteString(fmt.Sprintf(`func create%sHandler(w http.ResponseWriter, r *http.Request) { + var item %s + if err := json.NewDecoder(r.Body).Decode(&item); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + item.ID = uuid.New().String() + item.CreatedAt = time.Now() + item.UpdatedAt = time.Now() + + db.mu.Lock() + db.%s[item.ID] = &item + db.mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(item) +} + +`, entityName, entityName, entityPlural)) + + // Update handler + code.WriteString(fmt.Sprintf(`func update%sHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + db.mu.Lock() + defer db.mu.Unlock() + + existing, exists := db.%s[id] + if !exists { + http.Error(w, "%s not found", http.StatusNotFound) + return + } + + var updates %s + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Preserve ID and timestamps + updates.ID = existing.ID + updates.CreatedAt = existing.CreatedAt + updates.UpdatedAt = time.Now() + + db.%s[id] = &updates + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(updates) +} + +`, entityName, entityPlural, entityName, entityName, entityPlural)) + + // Delete handler + code.WriteString(fmt.Sprintf(`func delete%sHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + db.mu.Lock() + defer db.mu.Unlock() + + if _, exists := db.%s[id]; !exists { + http.Error(w, "%s not found", http.StatusNotFound) + return + } + + delete(db.%s, id) + w.WriteHeader(http.StatusNoContent) +} + +`, entityName, entityPlural, entityName, entityPlural)) + + return code.String() +} + +// generateCustomEndpointHandler creates handlers for custom endpoints +func (si *ServerInterpreter) generateCustomEndpointHandler(endpoint *lang.Endpoint) string { + var code strings.Builder + + handlerName := fmt.Sprintf("%s%sHandler", + strings.ToLower(endpoint.Method), + strings.ReplaceAll(cases.Title(language.English).String(endpoint.Path), "/", "")) + + code.WriteString(fmt.Sprintf(`func %s(w http.ResponseWriter, r *http.Request) { + // Custom endpoint: %s %s + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "message": "Custom endpoint %s %s", + "status": "success", + }) +} + +`, handlerName, endpoint.Method, endpoint.Path, endpoint.Method, endpoint.Path)) + + return code.String() +} + +// generateMainFunction creates the main function with routing +func (si *ServerInterpreter) generateMainFunction() string { + var code strings.Builder + + code.WriteString("func main() {\n") + code.WriteString("\tr := mux.NewRouter()\n\n") + + // Add routes for each entity + for entityName := range si.entities { + entityLower := strings.ToLower(entityName) + entityPlural := entityLower + "s" + + code.WriteString(fmt.Sprintf("\t// %s routes\n", entityName)) + code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s\", list%sHandler).Methods(\"GET\")\n", + entityPlural, entityName)) + code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", get%sHandler).Methods(\"GET\")\n", + entityPlural, entityName)) + code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s\", create%sHandler).Methods(\"POST\")\n", + entityPlural, entityName)) + code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", update%sHandler).Methods(\"PUT\")\n", + entityPlural, entityName)) + code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", delete%sHandler).Methods(\"DELETE\")\n", + entityPlural, entityName)) + code.WriteString("\n") + } + + // Add custom endpoint routes + for _, endpoint := range si.endpoints { + if endpoint.Entity == nil { + handlerName := fmt.Sprintf("%s%sHandler", + strings.ToLower(endpoint.Method), + strings.ReplaceAll(cases.Title(language.English).String(endpoint.Path), "/", "")) + code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"%s\", %s).Methods(\"%s\")\n", + endpoint.Path, handlerName, endpoint.Method)) + } + } + + // Server configuration + host := "localhost" + port := 8080 + + if si.server != nil { + for _, setting := range si.server.Settings { + if setting.Host != nil { + host = *setting.Host + } + if setting.Port != nil { + port = *setting.Port + } + } + } + + code.WriteString(fmt.Sprintf("\n\taddr := \"%s:%d\"\n", host, port)) + code.WriteString("\tfmt.Printf(\"Server starting on %s\\n\", addr)\n") + code.WriteString("\tlog.Fatal(http.ListenAndServe(addr, r))\n") + code.WriteString("}\n") + + return code.String() +}