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() +}