460 lines
13 KiB
Go
460 lines
13 KiB
Go
package interpreter
|
|
|
|
import (
|
|
"fmt"
|
|
"go/format"
|
|
"masonry/lang"
|
|
"strconv"
|
|
"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"
|
|
"os"
|
|
"strconv"
|
|
"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))
|
|
}
|
|
}
|
|
|
|
// Generate server configuration code that handles env vars and defaults at runtime
|
|
code.WriteString("\n\t// Server configuration\n")
|
|
code.WriteString(si.generateServerConfigCode())
|
|
|
|
code.WriteString("\n\taddr := fmt.Sprintf(\"%s:%d\", host, port)\n")
|
|
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()
|
|
}
|
|
|
|
// generateServerConfigCode generates Go code that handles server configuration at runtime
|
|
func (si *ServerInterpreter) generateServerConfigCode() string {
|
|
var code strings.Builder
|
|
|
|
// Default values
|
|
hostDefault := "localhost"
|
|
portDefault := 8080
|
|
var hostConfigValueCode, portConfigValueCode string
|
|
|
|
if si.server != nil {
|
|
for _, setting := range si.server.Settings {
|
|
if setting.Host != nil {
|
|
if setting.Host.EnvVar != nil && setting.Host.EnvVar.Default != nil {
|
|
hostDefault = *setting.Host.EnvVar.Default
|
|
}
|
|
hostConfigValueCode = si.generateConfigValueCode("host", setting.Host)
|
|
}
|
|
if setting.Port != nil {
|
|
if setting.Port.EnvVar != nil && setting.Port.EnvVar.Default != nil {
|
|
if defInt, err := strconv.Atoi(*setting.Port.EnvVar.Default); err == nil {
|
|
portDefault = defInt
|
|
}
|
|
}
|
|
portConfigValueCode = si.generateIntValueCode("port", setting.Port)
|
|
}
|
|
}
|
|
}
|
|
code.WriteString(fmt.Sprintf("\thost := \"%s\"\n", hostDefault))
|
|
code.WriteString(fmt.Sprintf("\tport := %d\n\n", portDefault))
|
|
code.WriteString(hostConfigValueCode)
|
|
code.WriteString(portConfigValueCode)
|
|
|
|
return code.String()
|
|
}
|
|
|
|
// generateConfigValueCode generates Go code to resolve a ConfigValue at runtime
|
|
func (si *ServerInterpreter) generateConfigValueCode(varName string, configValue *lang.ConfigValue) string {
|
|
var code strings.Builder
|
|
|
|
if configValue.Literal != nil {
|
|
// Simple literal assignment
|
|
code.WriteString(fmt.Sprintf("\t%s = %q\n", varName, *configValue.Literal))
|
|
} else if configValue.EnvVar != nil {
|
|
// Environment variable with optional default
|
|
code.WriteString(fmt.Sprintf("\tif envVal := os.Getenv(%q); envVal != \"\" {\n", configValue.EnvVar.Name))
|
|
code.WriteString(fmt.Sprintf("\t\t%s = envVal\n", varName))
|
|
|
|
if configValue.EnvVar.Default != nil {
|
|
code.WriteString("\t} else {\n")
|
|
code.WriteString(fmt.Sprintf("\t\t%s = %q\n", varName, *configValue.EnvVar.Default))
|
|
}
|
|
code.WriteString("\t}\n")
|
|
}
|
|
|
|
return code.String()
|
|
}
|
|
|
|
// generateIntValueCode generates Go code to resolve an IntValue at runtime
|
|
func (si *ServerInterpreter) generateIntValueCode(varName string, intValue *lang.IntValue) string {
|
|
var code strings.Builder
|
|
|
|
if intValue.Literal != nil {
|
|
// Simple literal assignment
|
|
code.WriteString(fmt.Sprintf("\t%s = %d\n", varName, *intValue.Literal))
|
|
} else if intValue.EnvVar != nil {
|
|
// Environment variable with optional default
|
|
code.WriteString(fmt.Sprintf("\tif envVal := os.Getenv(%q); envVal != \"\" {\n", intValue.EnvVar.Name))
|
|
code.WriteString(fmt.Sprintf("\t\tif val, err := strconv.Atoi(envVal); err == nil {\n"))
|
|
code.WriteString(fmt.Sprintf("\t\t\t%s = val\n", varName))
|
|
code.WriteString("\t\t}\n")
|
|
|
|
if intValue.EnvVar.Default != nil {
|
|
code.WriteString("\t} else {\n")
|
|
code.WriteString(fmt.Sprintf("\t\t%s = %s\n", varName, *intValue.EnvVar.Default))
|
|
}
|
|
code.WriteString("\t}\n")
|
|
}
|
|
|
|
return code.String()
|
|
}
|