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. :)
387 lines
10 KiB
Go
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()
|
|
}
|