package config import ( "errors" "fmt" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" ) // Config represents a configuration loaded from a Lua file type Config struct { // Server settings LogLevel string Port int Debug bool // Directory paths RoutesDir string StaticDir string OverrideDir string LibDirs []string // Performance settings BufferSize int // Feature flags HTTPLoggingEnabled bool Watchers map[string]bool // Raw values map for backward compatibility and custom values values map[string]any } // New creates a new configuration with default values func New() *Config { return &Config{ // Server defaults LogLevel: "info", Port: 3117, Debug: false, // Directory defaults RoutesDir: "./routes", StaticDir: "./static", OverrideDir: "./override", LibDirs: []string{"./libs"}, // Performance defaults BufferSize: 20, // Feature flag defaults HTTPLoggingEnabled: true, Watchers: map[string]bool{ "routes": false, "static": false, "modules": false, }, // Initialize values map values: make(map[string]any), } } // Load loads configuration from a Lua file func Load(filePath string) (*Config, error) { // Create a new Lua state state := luajit.New() if state == nil { return nil, errors.New("failed to create Lua state") } defer state.Close() // Create config with default values config := New() // Execute the Lua file if err := state.DoFile(filePath); err != nil { return nil, fmt.Errorf("failed to load config file: %w", err) } // Extract values from the Lua state if err := extractGlobals(state, config); err != nil { return nil, err } return config, nil } // extractGlobals extracts global variables from the Lua state func extractGlobals(state *luajit.State, config *Config) error { // Get the globals table (_G) state.GetGlobal("_G") if !state.IsTable(-1) { state.Pop(1) return errors.New("failed to get globals table") } // Pre-populate with standard globals for reference checking stdGlobals := map[string]bool{ "_G": true, "_VERSION": true, "assert": true, "collectgarbage": true, "coroutine": true, "debug": true, "dofile": true, "error": true, "getmetatable": true, "io": true, "ipairs": true, "load": true, "loadfile": true, "loadstring": true, "math": true, "next": true, "os": true, "package": true, "pairs": true, "pcall": true, "print": true, "rawequal": true, "rawget": true, "rawset": true, "require": true, "select": true, "setmetatable": true, "string": true, "table": true, "tonumber": true, "tostring": true, "type": true, "unpack": true, "xpcall": true, // LuaJIT specific globals "jit": true, "bit": true, "ffi": true, "bit32": true, } // First, let's get the original globals to compare with user globals originalGlobals := make(map[string]bool) // Execute empty Lua state to get standard globals emptyState := luajit.New() if emptyState != nil { defer emptyState.Close() emptyState.GetGlobal("_G") emptyState.PushNil() // Start iteration for emptyState.Next(-2) { if emptyState.IsString(-2) { key := emptyState.ToString(-2) originalGlobals[key] = true } emptyState.Pop(1) // Pop value, leave key for next iteration } emptyState.Pop(1) // Pop _G } // Iterate through the globals table state.PushNil() // Start iteration for state.Next(-2) { // Stack now has key at -2 and value at -1 // Get key as string if !state.IsString(-2) { state.Pop(1) // Pop value, leave key for next iteration continue } key := state.ToString(-2) // Skip standard Lua globals, but only if they're not overridden by user // (standard globals will be functions or tables, user values usually aren't) valueType := state.GetType(-1) // Skip functions, userdata, and threads regardless of origin if valueType == luajit.TypeFunction || valueType == luajit.TypeUserData || valueType == luajit.TypeThread { state.Pop(1) continue } // For known Lua globals, we need to see if they're the original or user-defined if stdGlobals[key] { // For simple value types, assume user-defined if valueType == luajit.TypeBoolean || valueType == luajit.TypeNumber || valueType == luajit.TypeString { // These are probably user values with standard names } else if originalGlobals[key] { // If it's in the original globals and not a simple type, skip it state.Pop(1) continue } } // Process based on key and type processConfigValue(state, config, key, valueType) } // Pop the globals table state.Pop(1) return nil } // processConfigValue processes a specific config value from Lua func processConfigValue(state *luajit.State, config *Config, key string, valueType luajit.LuaType) { // Store in the values map first (for backward compatibility) var value any // Extract the value based on its type switch valueType { case luajit.TypeBoolean: value = state.ToBoolean(-1) case luajit.TypeNumber: value = state.ToNumber(-1) case luajit.TypeString: value = state.ToString(-1) case luajit.TypeTable: // For tables, use the existing conversion logic if table, err := state.ToTable(-1); err == nil { value = table // Special case for watchers table if key == "watchers" { processWatchersTable(config, table) } } default: // Skip unsupported types state.Pop(1) return } // Store in the values map config.values[key] = value // Now set specific struct fields based on key switch key { case "log_level": if strVal, ok := value.(string); ok { config.LogLevel = strVal } case "port": if numVal, ok := value.(float64); ok { config.Port = int(numVal) } case "debug": if boolVal, ok := value.(bool); ok { config.Debug = boolVal } case "routes_dir": if strVal, ok := value.(string); ok { config.RoutesDir = strVal } case "static_dir": if strVal, ok := value.(string); ok { config.StaticDir = strVal } case "override_dir": if strVal, ok := value.(string); ok { config.OverrideDir = strVal } case "buffer_size": if numVal, ok := value.(float64); ok { config.BufferSize = int(numVal) } case "http_logging_enabled": if boolVal, ok := value.(bool); ok { config.HTTPLoggingEnabled = boolVal } case "lib_dirs": // Handle lib_dirs array processLibDirs(config, value) } state.Pop(1) // Pop value, leave key for next iteration } // processWatchersTable processes the watchers table configuration func processWatchersTable(config *Config, watchersTable map[string]any) { for key, value := range watchersTable { if boolVal, ok := value.(bool); ok { config.Watchers[key] = boolVal } } } // processLibDirs processes the lib_dirs array configuration func processLibDirs(config *Config, value any) { // Check if it's a direct array if arr, ok := value.([]any); ok { result := make([]string, 0, len(arr)) for _, v := range arr { if str, ok := v.(string); ok { result = append(result, str) } } if len(result) > 0 { config.LibDirs = result } return } // Check if it's in our special array format (map with empty key) valueMap, ok := value.(map[string]any) if !ok { return } arr, ok := valueMap[""] if !ok { return } // Handle array format if strArray := extractStringArray(arr); len(strArray) > 0 { config.LibDirs = strArray } } // extractStringArray extracts a string array from various possible formats func extractStringArray(arr any) []string { // Check different possible array formats switch arr := arr.(type) { case []string: return arr case []any: result := make([]string, 0, len(arr)) for _, v := range arr { if str, ok := v.(string); ok { result = append(result, str) } } return result case []float64: // Unlikely but handle numeric arrays too result := make([]string, 0, len(arr)) for _, v := range arr { result = append(result, fmt.Sprintf("%g", v)) } return result } return nil } // Get returns a configuration value by key func (c *Config) Get(key string) any { return c.values[key] } // GetString returns a string configuration value func (c *Config) GetString(key string, defaultValue string) string { // Check for specific struct fields first switch key { case "log_level": return c.LogLevel case "routes_dir": return c.RoutesDir case "static_dir": return c.StaticDir case "override_dir": return c.OverrideDir } // Fall back to values map for other keys value, ok := c.values[key] if !ok { return defaultValue } str, ok := value.(string) if !ok { return defaultValue } return str } // GetInt returns an integer configuration value func (c *Config) GetInt(key string, defaultValue int) int { // Check for specific struct fields first switch key { case "port": return c.Port case "buffer_size": return c.BufferSize } // Fall back to values map for other keys value, ok := c.values[key] if !ok { return defaultValue } // Handle both int and float64 (which is what Lua numbers become in Go) switch v := value.(type) { case int: return v case float64: return int(v) default: return defaultValue } } // GetFloat returns a float configuration value func (c *Config) GetFloat(key string, defaultValue float64) float64 { value, ok := c.values[key] if !ok { return defaultValue } // Handle both float64 and int switch v := value.(type) { case float64: return v case int: return float64(v) default: return defaultValue } } // GetBool returns a boolean configuration value func (c *Config) GetBool(key string, defaultValue bool) bool { // Check for specific struct fields first switch key { case "debug": return c.Debug case "http_logging_enabled": return c.HTTPLoggingEnabled } // Special case for watcher settings if key == "watchers.routes" { return c.Watchers["routes"] } else if key == "watchers.static" { return c.Watchers["static"] } else if key == "watchers.modules" { return c.Watchers["modules"] } // Fall back to values map for other keys value, ok := c.values[key] if !ok { return defaultValue } boolValue, ok := value.(bool) if !ok { return defaultValue } return boolValue } // GetMap returns a map configuration value func (c *Config) GetMap(key string) map[string]any { // Special case for watchers if key == "watchers" { result := make(map[string]any) for k, v := range c.Watchers { result[k] = v } return result } value, ok := c.values[key] if !ok { return nil } table, ok := value.(map[string]any) if !ok { return nil } return table } // GetArray returns an array of values from a Lua array func (c *Config) GetArray(key string) []any { // Special case for lib_dirs if key == "lib_dirs" { result := make([]any, len(c.LibDirs)) for i, v := range c.LibDirs { result[i] = v } return result } value := c.Get(key) if value == nil { return nil } // Direct array if arr, ok := value.([]any); ok { return arr } // Arrays in Lua might also be represented as maps with an empty string key valueMap, ok := value.(map[string]any) if !ok { return nil } // The array data is stored with an empty key arr, ok := valueMap[""] if !ok { return nil } // Check if it's a float64 array (common for Lua numeric arrays) if floatArr, ok := arr.([]float64); ok { // Convert to []any result := make([]any, len(floatArr)) for i, v := range floatArr { result[i] = v } return result } // Otherwise, try to return as is anyArr, ok := arr.([]any) if !ok { return nil } return anyArr } // GetIntArray returns an array of integers from a Lua array func (c *Config) GetIntArray(key string) []int { value := c.Get(key) if value == nil { return nil } // Direct array case if arr, ok := value.([]any); ok { result := make([]int, 0, len(arr)) for _, v := range arr { if num, ok := v.(float64); ok { result = append(result, int(num)) } } return result } // Arrays in Lua might also be represented as maps with an empty string key valueMap, ok := value.(map[string]any) if !ok { return nil } // The array data is stored with an empty key arr, ok := valueMap[""] if !ok { return nil } // For numeric arrays, LuaJIT returns []float64 floatArr, ok := arr.([]float64) if !ok { return nil } // Convert to int slice result := make([]int, len(floatArr)) for i, v := range floatArr { result[i] = int(v) } return result } // GetStringArray returns an array of strings from a Lua array func (c *Config) GetStringArray(key string) []string { // Special case for lib_dirs if key == "lib_dirs" { return c.LibDirs } arr := c.GetArray(key) if arr == nil { return nil } result := make([]string, 0, len(arr)) for _, v := range arr { if str, ok := v.(string); ok { result = append(result, str) } } return result } // Values returns all configuration values // Note: The returned map should not be modified func (c *Config) Values() map[string]any { return c.values } // Set sets a configuration value func (c *Config) Set(key string, value any) { c.values[key] = value // Also update the struct field if applicable switch key { case "log_level": if strVal, ok := value.(string); ok { c.LogLevel = strVal } case "port": if numVal, ok := value.(float64); ok { c.Port = int(numVal) } else if intVal, ok := value.(int); ok { c.Port = intVal } case "debug": if boolVal, ok := value.(bool); ok { c.Debug = boolVal } case "routes_dir": if strVal, ok := value.(string); ok { c.RoutesDir = strVal } case "static_dir": if strVal, ok := value.(string); ok { c.StaticDir = strVal } case "override_dir": if strVal, ok := value.(string); ok { c.OverrideDir = strVal } case "buffer_size": if numVal, ok := value.(float64); ok { c.BufferSize = int(numVal) } else if intVal, ok := value.(int); ok { c.BufferSize = intVal } case "http_logging_enabled": if boolVal, ok := value.(bool); ok { c.HTTPLoggingEnabled = boolVal } } }