add support for env variables to the DSL

This commit is contained in:
2025-09-02 00:54:38 -06:00
parent c6f14e1787
commit 69f507f176
12 changed files with 842 additions and 84 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/masonry.exe /masonry.exe
/.idea/copilotDiffState.xml

View File

@ -68,15 +68,9 @@ func main() {
// -- end of local database code -- // -- end of local database code --
// Uncomment these lines if you need automatic migration // Uncomment these lines if you need automatic migration
// err = gormDB.AutoMigrate(&pb.UserORM{}) // err = gormDB.AutoMigrate(&pb.UserORM{}, &pb.ProductORM{})
// if err != nil { // if err != nil {
// logger.Printf("failed to migrate user: %s", err) // logger.Printf("failed to auto migrate database: %s", err)
// log.Fatalln(err)
// return
// }
// err = gormDB.AutoMigrate(&pb.ProductORM{})
// if err != nil {
// logger.Printf("failed to migrate product: %s", err)
// log.Fatalln(err) // log.Fatalln(err)
// return // return
// } // }

View File

@ -140,23 +140,9 @@ func (hi *HTMLInterpreter) generatePageHTML(page *lang.Page) (string, error) {
// JavaScript for interactivity // JavaScript for interactivity
html.WriteString(" <script>\n") html.WriteString(" <script>\n")
// API Base URL configuration // Generate server configuration code that handles env vars and defaults at runtime
apiBaseURL := "http://localhost:8080" html.WriteString(hi.generateServerConfigJS())
if hi.server != nil {
host := "localhost"
port := 8080
for _, setting := range hi.server.Settings {
if setting.Host != nil {
host = *setting.Host
}
if setting.Port != nil {
port = *setting.Port
}
}
apiBaseURL = fmt.Sprintf("http://%s:%d", host, port)
}
html.WriteString(fmt.Sprintf(" const API_BASE_URL = '%s';\n", apiBaseURL))
html.WriteString(" \n") html.WriteString(" \n")
html.WriteString(" // API helper functions\n") html.WriteString(" // API helper functions\n")
html.WriteString(" async function apiRequest(method, endpoint, data = null) {\n") html.WriteString(" async function apiRequest(method, endpoint, data = null) {\n")
@ -348,6 +334,86 @@ func (hi *HTMLInterpreter) generatePageHTML(page *lang.Page) (string, error) {
return html.String(), nil return html.String(), nil
} }
// generateServerConfigJS generates JavaScript code that handles server configuration at runtime
func (hi *HTMLInterpreter) generateServerConfigJS() string {
var js strings.Builder
// Default API base URL
js.WriteString(" // Server configuration\n")
js.WriteString(" let apiHost = 'localhost';\n")
js.WriteString(" let apiPort = 8080;\n\n")
if hi.server != nil {
for _, setting := range hi.server.Settings {
if setting.Host != nil {
js.WriteString(hi.generateConfigValueJS("apiHost", setting.Host))
}
if setting.Port != nil {
js.WriteString(hi.generateIntValueJS("apiPort", setting.Port))
}
}
}
js.WriteString(" const API_BASE_URL = `http://${apiHost}:${apiPort}`;\n")
js.WriteString(" console.log('API Base URL:', API_BASE_URL);\n")
return js.String()
}
// generateConfigValueJS generates JavaScript code to resolve a ConfigValue at runtime
func (hi *HTMLInterpreter) generateConfigValueJS(varName string, configValue *lang.ConfigValue) string {
var js strings.Builder
if configValue.Literal != nil {
// Simple literal assignment
js.WriteString(fmt.Sprintf(" %s = %q;\n", varName, *configValue.Literal))
} else if configValue.EnvVar != nil {
// Environment variable resolution in browser (note: this is limited in browsers)
// For client-side, we'll need to use a different approach since browsers can't access server env vars
// We'll generate code that looks for the env var in localStorage or a global config object
js.WriteString(fmt.Sprintf(" // Check for %s in global config or localStorage\n", configValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" if (window.CONFIG && window.CONFIG['%s']) {\n", configValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" %s = window.CONFIG['%s'];\n", varName, configValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" } else if (localStorage.getItem('%s')) {\n", configValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" %s = localStorage.getItem('%s');\n", varName, configValue.EnvVar.Name))
if configValue.EnvVar.Default != nil {
js.WriteString(" } else {\n")
js.WriteString(fmt.Sprintf(" %s = %q;\n", varName, *configValue.EnvVar.Default))
}
js.WriteString(" }\n")
}
return js.String()
}
// generateIntValueJS generates JavaScript code to resolve an IntValue at runtime
func (hi *HTMLInterpreter) generateIntValueJS(varName string, intValue *lang.IntValue) string {
var js strings.Builder
if intValue.Literal != nil {
// Simple literal assignment
js.WriteString(fmt.Sprintf(" %s = %d;\n", varName, *intValue.Literal))
} else if intValue.EnvVar != nil {
// Environment variable resolution for integers
js.WriteString(fmt.Sprintf(" // Check for %s in global config or localStorage\n", intValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" if (window.CONFIG && window.CONFIG['%s']) {\n", intValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" const val = parseInt(window.CONFIG['%s']);\n", intValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" if (!isNaN(val)) %s = val;\n", varName))
js.WriteString(fmt.Sprintf(" } else if (localStorage.getItem('%s')) {\n", intValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" const val = parseInt(localStorage.getItem('%s'));\n", intValue.EnvVar.Name))
js.WriteString(fmt.Sprintf(" if (!isNaN(val)) %s = val;\n", varName))
if intValue.EnvVar.Default != nil {
js.WriteString(" } else {\n")
js.WriteString(fmt.Sprintf(" %s = %s;\n", varName, *intValue.EnvVar.Default))
}
js.WriteString(" }\n")
}
return js.String()
}
// generateSectionHTML creates HTML for a section // generateSectionHTML creates HTML for a section
func (hi *HTMLInterpreter) generateSectionHTML(section *lang.Section, indent int) (string, error) { func (hi *HTMLInterpreter) generateSectionHTML(section *lang.Section, indent int) (string, error) {
var html strings.Builder var html strings.Builder

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"go/format" "go/format"
"masonry/lang" "masonry/lang"
"strconv"
"strings" "strings"
"golang.org/x/text/cases" "golang.org/x/text/cases"
@ -69,6 +70,8 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os"
"strconv"
"sync" "sync"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -362,25 +365,95 @@ func (si *ServerInterpreter) generateMainFunction() string {
} }
} }
// Server configuration // Generate server configuration code that handles env vars and defaults at runtime
host := "localhost" code.WriteString("\n\t// Server configuration\n")
port := 8080 code.WriteString(si.generateServerConfigCode())
if si.server != nil { code.WriteString("\n\taddr := fmt.Sprintf(\"%s:%d\", host, port)\n")
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("\tfmt.Printf(\"Server starting on %s\\n\", addr)\n")
code.WriteString("\tlog.Fatal(http.ListenAndServe(addr, r))\n") code.WriteString("\tlog.Fatal(http.ListenAndServe(addr, r))\n")
code.WriteString("}\n") code.WriteString("}\n")
return code.String() 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()
}

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"text/template" "text/template"
@ -115,28 +116,58 @@ func (ti *TemplateInterpreter) Interpret(masonryInput string, tmplText string) (
"getHost": func(settings []lang.ServerSetting) string { "getHost": func(settings []lang.ServerSetting) string {
for _, s := range settings { for _, s := range settings {
if s.Host != nil { if s.Host != nil {
return *s.Host if s.Host.Literal != nil {
return "\"" + *s.Host.Literal + "\""
}
return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Host.EnvVar.Name, func() string {
if s.Host.EnvVar.Default != nil {
return *s.Host.EnvVar.Default
}
return "localhost"
}())
} }
} }
return "localhost" return "localhost"
}, },
"getPort": func(settings []lang.ServerSetting) int { "getPort": func(settings []lang.ServerSetting) string {
for _, s := range settings { for _, s := range settings {
if s.Port != nil { if s.Port != nil {
return *s.Port if s.Port.Literal != nil {
return strconv.Itoa(*s.Port.Literal)
}
return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Port.EnvVar.Name, func() string {
if s.Port.EnvVar.Default != nil {
return *s.Port.EnvVar.Default
}
return "8080"
}())
} }
} }
return 8080 return "8080"
}, },
"getServerHostPort": func(settings []lang.ServerSetting) string { "getServerHostPort": func(settings []lang.ServerSetting) string {
host := "localhost" host := "localhost"
port := 8080 port := 8080
for _, s := range settings { for _, s := range settings {
if s.Host != nil { if s.Host != nil {
host = *s.Host if s.Host.Literal != nil {
host = *s.Host.Literal
}
if s.Host.EnvVar != nil && s.Host.EnvVar.Default != nil {
host = *s.Host.EnvVar.Default
}
// If it's an env var, keep the default
} }
if s.Port != nil { if s.Port != nil {
port = *s.Port if s.Port.Literal != nil {
port = *s.Port.Literal
}
if s.Port.EnvVar != nil && s.Port.EnvVar.Default != nil {
if p, err := strconv.Atoi(*s.Port.EnvVar.Default); err == nil {
port = p
}
}
// If it's an env var, keep the default
} }
} }
return fmt.Sprintf("%s:%d", host, port) return fmt.Sprintf("%s:%d", host, port)
@ -234,28 +265,58 @@ func NewTemplateRegistry() *TemplateRegistry {
"getHost": func(settings []lang.ServerSetting) string { "getHost": func(settings []lang.ServerSetting) string {
for _, s := range settings { for _, s := range settings {
if s.Host != nil { if s.Host != nil {
return *s.Host if s.Host.Literal != nil {
return "\"" + *s.Host.Literal + "\""
}
return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Host.EnvVar.Name, func() string {
if s.Host.EnvVar.Default != nil {
return *s.Host.EnvVar.Default
}
return "localhost"
}())
} }
} }
return "localhost" return "localhost"
}, },
"getPort": func(settings []lang.ServerSetting) int { "getPort": func(settings []lang.ServerSetting) string {
for _, s := range settings { for _, s := range settings {
if s.Port != nil { if s.Port != nil {
return *s.Port if s.Port.Literal != nil {
return strconv.Itoa(*s.Port.Literal)
}
return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Port.EnvVar.Name, func() string {
if s.Port.EnvVar.Default != nil {
return *s.Port.EnvVar.Default
}
return "8080"
}())
} }
} }
return 8080 return "8080"
}, },
"getServerHostPort": func(settings []lang.ServerSetting) string { "getServerHostPort": func(settings []lang.ServerSetting) string {
host := "localhost" host := "localhost"
port := 8080 port := 8080
for _, s := range settings { for _, s := range settings {
if s.Host != nil { if s.Host != nil {
host = *s.Host if s.Host.Literal != nil {
host = *s.Host.Literal
}
if s.Host.EnvVar != nil && s.Host.EnvVar.Default != nil {
host = *s.Host.EnvVar.Default
}
// If it's an env var, keep the default
} }
if s.Port != nil { if s.Port != nil {
port = *s.Port if s.Port.Literal != nil {
port = *s.Port.Literal
}
if s.Port.EnvVar != nil && s.Port.EnvVar.Default != nil {
if p, err := strconv.Atoi(*s.Port.EnvVar.Default); err == nil {
port = p
}
}
// If it's an env var, keep the default
} }
} }
return fmt.Sprintf("%s:%d", host, port) return fmt.Sprintf("%s:%d", host, port)

98
lang/debug_env_test.go Normal file
View File

@ -0,0 +1,98 @@
package lang
import (
"testing"
)
func TestSimpleEnvVar(t *testing.T) {
// Test the simplest possible environment variable syntax
input := `
server MyApp {
host env "HOST"
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
server := ast.Definitions[0].Server
if server.Settings[0].Host.EnvVar == nil {
t.Fatalf("Expected environment variable")
}
if server.Settings[0].Host.EnvVar.Name != "HOST" {
t.Errorf("Expected HOST, got %s", server.Settings[0].Host.EnvVar.Name)
}
}
func TestLiteralValue(t *testing.T) {
// Test that literal values still work
input := `
server MyApp {
host "localhost"
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
server := ast.Definitions[0].Server
if server.Settings[0].Host.Literal == nil {
t.Fatalf("Expected literal value")
}
if *server.Settings[0].Host.Literal != "localhost" {
t.Errorf("Expected localhost, got %s", *server.Settings[0].Host.Literal)
}
}
func TestEnvVarWithDefault(t *testing.T) {
// Test environment variable with default value
input := `
server MyApp {
host env "HOST" default "localhost"
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
server := ast.Definitions[0].Server
envVar := server.Settings[0].Host.EnvVar
if envVar == nil {
t.Fatalf("Expected environment variable")
}
if envVar.Name != "HOST" {
t.Errorf("Expected HOST, got %s", envVar.Name)
}
if envVar.Default == nil || *envVar.Default != "localhost" {
t.Errorf("Expected default localhost")
}
}
func TestEnvVarRequired(t *testing.T) {
// Test environment variable with required flag
input := `
server MyApp {
api_key env "API_KEY" required
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
server := ast.Definitions[0].Server
envVar := server.Settings[0].APIKey.EnvVar
if envVar == nil {
t.Fatalf("Expected environment variable")
}
if !envVar.Required {
t.Errorf("Expected required to be true")
}
}

View File

@ -4,12 +4,12 @@ import (
"github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2"
) )
// Root AST node containing all definitions // AST Root AST node containing all definitions
type AST struct { type AST struct {
Definitions []Definition `parser:"@@*"` Definitions []Definition `parser:"@@*"`
} }
// Union type for top-level definitions // Definition Union type for top-level definitions
type Definition struct { type Definition struct {
Server *Server `parser:"@@"` Server *Server `parser:"@@"`
Entity *Entity `parser:"| @@"` Entity *Entity `parser:"| @@"`
@ -17,25 +17,48 @@ type Definition struct {
Page *Page `parser:"| @@"` Page *Page `parser:"| @@"`
} }
// Clean server syntax // ConfigValue Flexible value that can be literal or environment variable
type ConfigValue struct {
Literal *string `parser:"@String"`
EnvVar *EnvVar `parser:"| @@"`
}
// EnvVar Environment variable configuration
type EnvVar struct {
Name string `parser:"'env' @String"`
Default *string `parser:"('default' @String)?"`
Required bool `parser:"@'required'?"`
}
// Server Clean server syntax
type Server struct { type Server struct {
Name string `parser:"'server' @Ident"` Name string `parser:"'server' @Ident"`
Settings []ServerSetting `parser:"('{' @@* '}')?"` // Block-delimited settings for consistency Settings []ServerSetting `parser:"('{' @@* '}')?"` // Block-delimited settings for consistency
} }
type ServerSetting struct { type ServerSetting struct {
Host *string `parser:"('host' @String)"` Host *ConfigValue `parser:"('host' @@)"`
Port *int `parser:"| ('port' @Int)"` Port *IntValue `parser:"| ('port' @@)"`
DatabaseURL *ConfigValue `parser:"| ('database_url' @@)"`
APIKey *ConfigValue `parser:"| ('api_key' @@)"`
SSLCert *ConfigValue `parser:"| ('ssl_cert' @@)"`
SSLKey *ConfigValue `parser:"| ('ssl_key' @@)"`
} }
// Clean entity syntax with better readability // IntValue Similar to ConfigValue but for integers
type IntValue struct {
Literal *int `parser:"@Int"`
EnvVar *EnvVar `parser:"| @@"`
}
// Entity Clean entity syntax with better readability
type Entity struct { type Entity struct {
Name string `parser:"'entity' @Ident"` Name string `parser:"'entity' @Ident"`
Description *string `parser:"('desc' @String)?"` Description *string `parser:"('desc' @String)?"`
Fields []Field `parser:"('{' @@* '}')?"` // Block-delimited fields for consistency Fields []Field `parser:"('{' @@* '}')?"` // Block-delimited fields for consistency
} }
// Much cleaner field syntax // Field Detailed field syntax
type Field struct { type Field struct {
Name string `parser:"@Ident ':'"` Name string `parser:"@Ident ':'"`
Type string `parser:"@Ident"` Type string `parser:"@Ident"`
@ -45,15 +68,24 @@ type Field struct {
Default *string `parser:"('default' @String)?"` Default *string `parser:"('default' @String)?"`
Validations []Validation `parser:"@@*"` Validations []Validation `parser:"@@*"`
Relationship *Relationship `parser:"@@?"` Relationship *Relationship `parser:"@@?"`
Endpoints []string `parser:"('endpoints' '[' @Ident (',' @Ident)* ']')?"` // with transforms this might not be needed
Transform []Transform `parser:"@@*"`
} }
// Simple validation syntax // Transform Field transformation specification
type Transform struct {
Type string `parser:"'transform' @Ident"`
Column *string `parser:"('to' @Ident)?"`
Direction *string `parser:"('on' @('input' | 'output' | 'both'))?"`
}
// Validation Simple validation syntax
type Validation struct { type Validation struct {
Type string `parser:"'validate' @Ident"` Type string `parser:"'validate' @Ident"`
Value *string `parser:"@String?"` Value *string `parser:"@String?"`
} }
// Clear relationship syntax // Relationship Clear relationship syntax
type Relationship struct { type Relationship struct {
Type string `parser:"'relates' 'to' @Ident"` Type string `parser:"'relates' 'to' @Ident"`
Cardinality string `parser:"'as' @('one' | 'many')"` Cardinality string `parser:"'as' @('one' | 'many')"`
@ -73,7 +105,7 @@ type Endpoint struct {
CustomLogic *string `parser:"('custom' @String)? '}')?"` // Close block after all content CustomLogic *string `parser:"('custom' @String)? '}')?"` // Close block after all content
} }
// Clean parameter syntax // EndpointParam Clean parameter syntax
type EndpointParam struct { type EndpointParam struct {
Name string `parser:"'param' @Ident ':'"` Name string `parser:"'param' @Ident ':'"`
Type string `parser:"@Ident"` Type string `parser:"@Ident"`
@ -81,14 +113,14 @@ type EndpointParam struct {
Source string `parser:"'from' @('path' | 'query' | 'body')"` Source string `parser:"'from' @('path' | 'query' | 'body')"`
} }
// Response specification // ResponseSpec Response specification
type ResponseSpec struct { type ResponseSpec struct {
Type string `parser:"'returns' @Ident"` Type string `parser:"'returns' @Ident"`
Format *string `parser:"('as' @String)?"` Format *string `parser:"('as' @String)?"`
Fields []string `parser:"('fields' '[' @Ident (',' @Ident)* ']')?"` Fields []string `parser:"('fields' '[' @Ident (',' @Ident)* ']')?"`
} }
// Enhanced Page definitions with unified section model // Page Enhanced Page definitions with unified section model
type Page struct { type Page struct {
Name string `parser:"'page' @Ident"` Name string `parser:"'page' @Ident"`
Path string `parser:"'at' @String"` Path string `parser:"'at' @String"`
@ -101,13 +133,13 @@ type Page struct {
Components []Component `parser:"@@* '}')?"` // Direct components within the block Components []Component `parser:"@@* '}')?"` // Direct components within the block
} }
// Meta tags for SEO // MetaTag Meta tags for SEO
type MetaTag struct { type MetaTag struct {
Name string `parser:"'meta' @Ident"` Name string `parser:"'meta' @Ident"`
Content string `parser:"@String"` Content string `parser:"@String"`
} }
// Unified Section type that replaces Container, Tab, Panel, Modal, MasterDetail // Section Unified Section type that replaces Container, Tab, Panel, Modal, MasterDetail
type Section struct { type Section struct {
Name string `parser:"'section' @Ident"` Name string `parser:"'section' @Ident"`
Type *string `parser:"('type' @('container' | 'tab' | 'panel' | 'modal' | 'master' | 'detail'))?"` Type *string `parser:"('type' @('container' | 'tab' | 'panel' | 'modal' | 'master' | 'detail'))?"`
@ -120,7 +152,7 @@ type Section struct {
Elements []SectionElement `parser:"('{' @@* '}')?"` // Block-delimited elements for unambiguous nesting Elements []SectionElement `parser:"('{' @@* '}')?"` // Block-delimited elements for unambiguous nesting
} }
// New unified element type for sections // SectionElement New unified element type for sections
type SectionElement struct { type SectionElement struct {
Attribute *SectionAttribute `parser:"@@"` Attribute *SectionAttribute `parser:"@@"`
Component *Component `parser:"| @@"` Component *Component `parser:"| @@"`
@ -128,7 +160,7 @@ type SectionElement struct {
When *WhenCondition `parser:"| @@"` When *WhenCondition `parser:"| @@"`
} }
// Flexible section attributes (replaces complex config types) // SectionAttribute Flexible section attributes (replaces complex config types)
type SectionAttribute struct { type SectionAttribute struct {
DataSource *string `parser:"('data' 'from' @String)"` DataSource *string `parser:"('data' 'from' @String)"`
Style *string `parser:"| ('style' @String)"` Style *string `parser:"| ('style' @String)"`
@ -137,14 +169,14 @@ type SectionAttribute struct {
Theme *string `parser:"| ('theme' @String)"` Theme *string `parser:"| ('theme' @String)"`
} }
// Simplified Component with unified attributes - reordered for better parsing // Component Simplified Component with unified attributes - reordered for better parsing
type Component struct { type Component struct {
Type string `parser:"'component' @Ident"` Type string `parser:"'component' @Ident"`
Entity *string `parser:"('for' @Ident)?"` Entity *string `parser:"('for' @Ident)?"`
Elements []ComponentElement `parser:"('{' @@* '}')?"` // Parse everything inside the block Elements []ComponentElement `parser:"('{' @@* '}')?"` // Parse everything inside the block
} }
// Enhanced ComponentElement with recursive section support - now includes attributes // ComponentElement Enhanced ComponentElement with recursive section support - now includes attributes
type ComponentElement struct { type ComponentElement struct {
Attribute *ComponentAttr `parser:"@@"` // Component attributes can be inside the block Attribute *ComponentAttr `parser:"@@"` // Component attributes can be inside the block
Field *ComponentField `parser:"| @@"` Field *ComponentField `parser:"| @@"`
@ -153,7 +185,7 @@ type ComponentElement struct {
When *WhenCondition `parser:"| @@"` When *WhenCondition `parser:"| @@"`
} }
// Simplified component attributes using key-value pattern - reordered for precedence // ComponentAttr Simplified component attributes using key-value pattern - reordered for precedence
type ComponentAttr struct { type ComponentAttr struct {
DataSource *string `parser:"('data' 'from' @String)"` DataSource *string `parser:"('data' 'from' @String)"`
Fields []string `parser:"| ('fields' '[' @Ident (',' @Ident)* ']')"` Fields []string `parser:"| ('fields' '[' @Ident (',' @Ident)* ']')"`
@ -164,14 +196,14 @@ type ComponentAttr struct {
Validate bool `parser:"| @'validate'"` Validate bool `parser:"| @'validate'"`
} }
// Enhanced component field with detailed configuration using flexible attributes // ComponentField Enhanced component field with detailed configuration using flexible attributes
type ComponentField struct { type ComponentField struct {
Name string `parser:"'field' @Ident"` Name string `parser:"'field' @Ident"`
Type string `parser:"'type' @Ident"` Type string `parser:"'type' @Ident"`
Attributes []ComponentFieldAttribute `parser:"@@* ('{' @@* '}')?"` // Support both inline and block attributes Attributes []ComponentFieldAttribute `parser:"@@* ('{' @@* '}')?"` // Support both inline and block attributes
} }
// Flexible field attribute system // ComponentFieldAttribute Flexible field attribute system
type ComponentFieldAttribute struct { type ComponentFieldAttribute struct {
Label *string `parser:"('label' @String)"` Label *string `parser:"('label' @String)"`
Placeholder *string `parser:"| ('placeholder' @String)"` Placeholder *string `parser:"| ('placeholder' @String)"`
@ -192,18 +224,18 @@ type ComponentFieldAttribute struct {
Validation *ComponentValidation `parser:"| @@"` Validation *ComponentValidation `parser:"| @@"`
} }
// Field relationship for autocomplete and select fields // FieldRelation Field relationship for autocomplete and select fields
type FieldRelation struct { type FieldRelation struct {
Type string `parser:"'relates' 'to' @Ident"` Type string `parser:"'relates' 'to' @Ident"`
} }
// Component validation // ComponentValidation Component validation
type ComponentValidation struct { type ComponentValidation struct {
Type string `parser:"'validate' @Ident"` Type string `parser:"'validate' @Ident"`
Value *string `parser:"@String?"` Value *string `parser:"@String?"`
} }
// Enhanced WhenCondition with recursive support for both sections and components // WhenCondition Enhanced WhenCondition with recursive support for both sections and components
type WhenCondition struct { type WhenCondition struct {
Field string `parser:"'when' @Ident"` Field string `parser:"'when' @Ident"`
Operator string `parser:"@('equals' | 'not_equals' | 'contains')"` Operator string `parser:"@('equals' | 'not_equals' | 'contains')"`
@ -214,14 +246,14 @@ type WhenCondition struct {
Buttons []ComponentButton `parser:"@@* '}')?"` // Block-delimited for unambiguous nesting Buttons []ComponentButton `parser:"@@* '}')?"` // Block-delimited for unambiguous nesting
} }
// Simplified button with flexible attribute ordering // ComponentButton Simplified button with flexible attribute ordering
type ComponentButton struct { type ComponentButton struct {
Name string `parser:"'button' @Ident"` Name string `parser:"'button' @Ident"`
Label string `parser:"'label' @String"` Label string `parser:"'label' @String"`
Attributes []ComponentButtonAttr `parser:"@@*"` Attributes []ComponentButtonAttr `parser:"@@*"`
} }
// Flexible button attribute system - each attribute is a separate alternative // ComponentButtonAttr Flexible button attribute system - each attribute is a separate alternative
type ComponentButtonAttr struct { type ComponentButtonAttr struct {
Style *ComponentButtonStyle `parser:"@@"` Style *ComponentButtonStyle `parser:"@@"`
Icon *ComponentButtonIcon `parser:"| @@"` Icon *ComponentButtonIcon `parser:"| @@"`
@ -233,7 +265,7 @@ type ComponentButtonAttr struct {
Via *ComponentButtonVia `parser:"| @@"` Via *ComponentButtonVia `parser:"| @@"`
} }
// Individual button attribute types // ComponentButtonStyle Individual button attribute types
type ComponentButtonStyle struct { type ComponentButtonStyle struct {
Value string `parser:"'style' @String"` Value string `parser:"'style' @String"`
} }

342
lang/parser_env_test.go Normal file
View File

@ -0,0 +1,342 @@
package lang
import (
"testing"
)
func TestServerWithEnvironmentVariables(t *testing.T) {
input := `
server MyApp {
host env "HOST" default "localhost"
port env "PORT" default "8080"
database_url env "DATABASE_URL" required
api_key env "API_SECRET_KEY" required
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
if len(ast.Definitions) != 1 {
t.Fatalf("Expected 1 definition, got %d", len(ast.Definitions))
}
server := ast.Definitions[0].Server
if server == nil {
t.Fatalf("Expected server definition")
}
if server.Name != "MyApp" {
t.Errorf("Expected server name 'MyApp', got '%s'", server.Name)
}
if len(server.Settings) != 4 {
t.Fatalf("Expected 4 settings, got %d", len(server.Settings))
}
// Test host setting
hostSetting := server.Settings[0]
if hostSetting.Host == nil {
t.Fatalf("Expected host setting")
}
if hostSetting.Host.EnvVar == nil {
t.Fatalf("Expected host to be environment variable")
}
if hostSetting.Host.EnvVar.Name != "HOST" {
t.Errorf("Expected env var name 'HOST', got '%s'", hostSetting.Host.EnvVar.Name)
}
if hostSetting.Host.EnvVar.Default == nil || *hostSetting.Host.EnvVar.Default != "localhost" {
t.Errorf("Expected default 'localhost'")
}
// Test port setting
portSetting := server.Settings[1]
if portSetting.Port == nil {
t.Fatalf("Expected port setting")
}
if portSetting.Port.EnvVar == nil {
t.Fatalf("Expected port to be environment variable")
}
if portSetting.Port.EnvVar.Name != "PORT" {
t.Errorf("Expected env var name 'PORT', got '%s'", portSetting.Port.EnvVar.Name)
}
if portSetting.Port.EnvVar.Default == nil || *portSetting.Port.EnvVar.Default != "8080" {
t.Errorf("Expected default '8080'")
}
// Test required database_url
dbSetting := server.Settings[2]
if dbSetting.DatabaseURL == nil {
t.Fatalf("Expected database_url setting")
}
if dbSetting.DatabaseURL.EnvVar == nil {
t.Fatalf("Expected database_url to be environment variable")
}
if !dbSetting.DatabaseURL.EnvVar.Required {
t.Errorf("Expected database_url to be required")
}
// Test required api_key
apiSetting := server.Settings[3]
if apiSetting.APIKey == nil {
t.Fatalf("Expected api_key setting")
}
if apiSetting.APIKey.EnvVar == nil {
t.Fatalf("Expected api_key to be environment variable")
}
if !apiSetting.APIKey.EnvVar.Required {
t.Errorf("Expected api_key to be required")
}
}
func TestServerWithMixedValues(t *testing.T) {
input := `
server MyApp {
host "localhost"
port env "PORT" default "8080"
database_url env "DATABASE_URL" required
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
server := ast.Definitions[0].Server
if server == nil {
t.Fatalf("Expected server definition")
}
// Test literal host
hostSetting := server.Settings[0]
if hostSetting.Host == nil {
t.Fatalf("Expected host setting")
}
if hostSetting.Host.Literal == nil {
t.Fatalf("Expected host to be literal value")
}
if *hostSetting.Host.Literal != "localhost" {
t.Errorf("Expected literal value 'localhost', got '%s'", *hostSetting.Host.Literal)
}
// Test env port
portSetting := server.Settings[1]
if portSetting.Port == nil {
t.Fatalf("Expected port setting")
}
if portSetting.Port.EnvVar == nil {
t.Fatalf("Expected port to be environment variable")
}
}
func TestServerWithLiteralPort(t *testing.T) {
input := `
server MyApp {
host "localhost"
port 8080
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
server := ast.Definitions[0].Server
portSetting := server.Settings[1]
if portSetting.Port == nil {
t.Fatalf("Expected port setting")
}
if portSetting.Port.Literal == nil {
t.Fatalf("Expected port to be literal value")
}
if *portSetting.Port.Literal != 8080 {
t.Errorf("Expected literal port 8080, got %d", *portSetting.Port.Literal)
}
}
func TestServerConfigurationsInAnyOrder(t *testing.T) {
// Test that server settings can be defined in any order
tests := []struct {
name string
input string
}{
{
name: "host first, then port",
input: `
server MyApp {
host "localhost"
port 8080
}`,
},
{
name: "port first, then host",
input: `
server MyApp {
port 8080
host "localhost"
}`,
},
{
name: "mixed literal and env vars in random order",
input: `
server MyApp {
api_key env "API_KEY" required
host "localhost"
database_url env "DATABASE_URL" default "postgres://localhost:5432/myapp"
port 8080
ssl_cert env "SSL_CERT" required
}`,
},
{
name: "all env vars in different order",
input: `
server MyApp {
ssl_key env "SSL_KEY" required
port env "PORT" default "8080"
database_url env "DATABASE_URL" required
host env "HOST" default "localhost"
api_key env "API_KEY" required
ssl_cert env "SSL_CERT" required
}`,
},
{
name: "all literal values in different order",
input: `
server MyApp {
database_url "postgres://localhost:5432/myapp"
port 3000
ssl_key "/path/to/ssl.key"
host "0.0.0.0"
api_key "secret123"
ssl_cert "/path/to/ssl.crt"
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ast, err := ParseInput(tt.input)
if err != nil {
t.Fatalf("Parse error for %s: %v", tt.name, err)
}
if len(ast.Definitions) != 1 {
t.Fatalf("Expected 1 definition, got %d", len(ast.Definitions))
}
server := ast.Definitions[0].Server
if server == nil {
t.Fatalf("Expected server definition")
}
if server.Name != "MyApp" {
t.Errorf("Expected server name 'MyApp', got '%s'", server.Name)
}
// Verify that we can parse any number of settings in any order
if len(server.Settings) < 1 {
t.Fatalf("Expected at least 1 setting, got %d", len(server.Settings))
}
// Test that we can access specific settings regardless of order
var hasHost, hasPort bool
for _, setting := range server.Settings {
if setting.Host != nil {
hasHost = true
}
if setting.Port != nil {
hasPort = true
}
}
// For the first two tests, verify both host and port are present
if tt.name == "host first, then port" || tt.name == "port first, then host" {
if !hasHost {
t.Errorf("Expected to find host setting")
}
if !hasPort {
t.Errorf("Expected to find port setting")
}
}
t.Logf("Successfully parsed %d settings for test '%s'", len(server.Settings), tt.name)
})
}
}
func TestServerConfigurationValidation(t *testing.T) {
// Test that we can properly validate different configurations
input := `
server ProductionApp {
ssl_cert env "SSL_CERT_PATH" required
database_url env "DATABASE_URL" required
host env "HOST" default "0.0.0.0"
api_key env "SECRET_API_KEY" required
port env "PORT" default "443"
ssl_key env "SSL_KEY_PATH" required
}
`
ast, err := ParseInput(input)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
server := ast.Definitions[0].Server
if server.Name != "ProductionApp" {
t.Errorf("Expected server name 'ProductionApp', got '%s'", server.Name)
}
// Create a map to easily check for specific settings
settingsMap := make(map[string]interface{})
for _, setting := range server.Settings {
if setting.Host != nil {
settingsMap["host"] = setting.Host
}
if setting.Port != nil {
settingsMap["port"] = setting.Port
}
if setting.DatabaseURL != nil {
settingsMap["database_url"] = setting.DatabaseURL
}
if setting.APIKey != nil {
settingsMap["api_key"] = setting.APIKey
}
if setting.SSLCert != nil {
settingsMap["ssl_cert"] = setting.SSLCert
}
if setting.SSLKey != nil {
settingsMap["ssl_key"] = setting.SSLKey
}
}
// Verify all expected settings are present
expectedSettings := []string{"host", "port", "database_url", "api_key", "ssl_cert", "ssl_key"}
for _, expected := range expectedSettings {
if _, exists := settingsMap[expected]; !exists {
t.Errorf("Expected to find setting '%s'", expected)
}
}
// Verify host has default value
if hostSetting, ok := settingsMap["host"].(*ConfigValue); ok {
if hostSetting.EnvVar == nil {
t.Errorf("Expected host to be environment variable")
} else if hostSetting.EnvVar.Default == nil || *hostSetting.EnvVar.Default != "0.0.0.0" {
t.Errorf("Expected host default to be '0.0.0.0'")
}
}
// Verify ssl_cert is required
if sslCertSetting, ok := settingsMap["ssl_cert"].(*ConfigValue); ok {
if sslCertSetting.EnvVar == nil {
t.Errorf("Expected ssl_cert to be environment variable")
} else if !sslCertSetting.EnvVar.Required {
t.Errorf("Expected ssl_cert to be required")
}
}
}

View File

@ -4,6 +4,35 @@ import (
"testing" "testing"
) )
// Helper functions for creating ConfigValue and IntValue instances
func literalConfigValue(value string) *ConfigValue {
return &ConfigValue{Literal: &value}
}
func literalIntValue(value int) *IntValue {
return &IntValue{Literal: &value}
}
func envConfigValue(name string, defaultValue *string, required bool) *ConfigValue {
return &ConfigValue{
EnvVar: &EnvVar{
Name: name,
Default: defaultValue,
Required: required,
},
}
}
func envIntValue(name string, defaultValue *string, required bool) *IntValue {
return &IntValue{
EnvVar: &EnvVar{
Name: name,
Default: defaultValue,
Required: required,
},
}
}
func TestParseServerDefinitions(t *testing.T) { func TestParseServerDefinitions(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -23,8 +52,8 @@ func TestParseServerDefinitions(t *testing.T) {
Server: &Server{ Server: &Server{
Name: "MyApp", Name: "MyApp",
Settings: []ServerSetting{ Settings: []ServerSetting{
{Host: stringPtr("localhost")}, {Host: literalConfigValue("localhost")},
{Port: intPtr(8080)}, {Port: literalIntValue(8080)},
}, },
}, },
}, },
@ -43,7 +72,7 @@ func TestParseServerDefinitions(t *testing.T) {
Server: &Server{ Server: &Server{
Name: "WebApp", Name: "WebApp",
Settings: []ServerSetting{ Settings: []ServerSetting{
{Host: stringPtr("0.0.0.0")}, {Host: literalConfigValue("0.0.0.0")},
}, },
}, },
}, },
@ -62,7 +91,7 @@ func TestParseServerDefinitions(t *testing.T) {
Server: &Server{ Server: &Server{
Name: "APIServer", Name: "APIServer",
Settings: []ServerSetting{ Settings: []ServerSetting{
{Port: intPtr(3000)}, {Port: literalIntValue(3000)},
}, },
}, },
}, },

View File

@ -21,7 +21,68 @@ func serverEqual(got, want Server) bool {
} }
func serverSettingEqual(got, want ServerSetting) bool { func serverSettingEqual(got, want ServerSetting) bool {
return stringPtrEqual(got.Host, want.Host) && intPtrEqual(got.Port, want.Port) return configValueEqual(got.Host, want.Host) &&
intValueEqual(got.Port, want.Port) &&
configValueEqual(got.DatabaseURL, want.DatabaseURL) &&
configValueEqual(got.APIKey, want.APIKey) &&
configValueEqual(got.SSLCert, want.SSLCert) &&
configValueEqual(got.SSLKey, want.SSLKey)
}
func configValueEqual(got, want *ConfigValue) bool {
if got == nil && want == nil {
return true
}
if got == nil || want == nil {
return false
}
// Check literal values
if got.Literal != nil && want.Literal != nil {
return *got.Literal == *want.Literal
}
if got.Literal != nil || want.Literal != nil {
return false
}
// Check environment variables
if got.EnvVar != nil && want.EnvVar != nil {
return envVarEqual(*got.EnvVar, *want.EnvVar)
}
return got.EnvVar == nil && want.EnvVar == nil
}
func intValueEqual(got, want *IntValue) bool {
if got == nil && want == nil {
return true
}
if got == nil || want == nil {
return false
}
// Check literal values
if got.Literal != nil && want.Literal != nil {
return *got.Literal == *want.Literal
}
if got.Literal != nil || want.Literal != nil {
return false
}
// Check environment variables
if got.EnvVar != nil && want.EnvVar != nil {
return envVarEqual(*got.EnvVar, *want.EnvVar)
}
return got.EnvVar == nil && want.EnvVar == nil
}
func envVarEqual(got, want EnvVar) bool {
if got.Name != want.Name {
return false
}
if got.Required != want.Required {
return false
}
return stringPtrEqual(got.Default, want.Default)
} }
func entityEqual(got, want Entity) bool { func entityEqual(got, want Entity) bool {

View File

@ -22,7 +22,8 @@ Masonry is a library that provides and implements all the basics necessary to bu
- [ ] Indicate values that are based on Environment variables - [ ] Indicate values that are based on Environment variables
- [ ] Yaml file to include custom template functions (Otto for JS) - [ ] Yaml file to include custom template functions (Otto for JS)
- [ ] On Entities, we should indicate what CRUD functions should be implemented instead of implementing all, or forcing the user to define each in the Endpoints section - [x] On Entities, we should indicate what CRUD functions should be implemented instead of implementing all, or forcing the user to define each in the Endpoints section
- [ ] Support field transformations on Entities using a template for `BeforeToORM` in `service.pb.gorm.override.go`
## Design Philosophy (changeable...) ## Design Philosophy (changeable...)

View File