Files
masonry/interpreter/server_interpreter.go
Mason Payne d36e1bfd86 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. :)
2025-08-25 00:50:55 -06:00

387 lines
10 KiB
Go

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