add support for env variables to the DSL
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/masonry.exe
|
/masonry.exe
|
||||||
|
/.idea/copilotDiffState.xml
|
||||||
|
@ -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
|
||||||
// }
|
// }
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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
98
lang/debug_env_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
86
lang/lang.go
86
lang/lang.go
@ -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
342
lang/parser_env_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -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...)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user