package config import ( "fmt" "os" "path/filepath" "sync" "github.com/BurntSushi/toml" ) type Config struct { Server ServerConfig `toml:"server"` Database DatabaseConfig `toml:"database"` JWT JWTConfig `toml:"jwt"` } type ServerConfig struct { BindAddr string `toml:"bind_addr"` Mode string `toml:"mode"` // "debug" or "release" } type DatabaseConfig struct { Type string `toml:"type"` // "sqlite", "postgres", "mysql" Path string `toml:"path"` // For SQLite Host string `toml:"host"` // For PostgreSQL/MySQL Port int `toml:"port"` // For PostgreSQL/MySQL User string `toml:"user"` // For PostgreSQL/MySQL Password string `toml:"password"` // For PostgreSQL/MySQL DBName string `toml:"dbname"` // For PostgreSQL/MySQL SSLMode string `toml:"sslmode"` // For PostgreSQL } type JWTConfig struct { Secret string `toml:"secret"` Expiration int `toml:"expiration"` // in hours } var ( instance *Config once sync.Once ) // Load loads the configuration from a TOML file func Load(configPath string) (*Config, error) { var err error once.Do(func() { instance = &Config{} if _, loadErr := toml.DecodeFile(configPath, instance); loadErr != nil { err = fmt.Errorf("failed to load configuration: %w", loadErr) return } // Default values if not specified if instance.Server.BindAddr == "" { instance.Server.BindAddr = "0.0.0.0:7000" } if instance.Server.Mode == "" { instance.Server.Mode = "release" } if instance.Database.Type == "" { instance.Database.Type = "sqlite" } if instance.Database.Path == "" { instance.Database.Path = "./oxspeak.db" } if instance.JWT.Expiration == 0 { instance.JWT.Expiration = 24 } // Validation if instance.JWT.Secret == "" { err = fmt.Errorf("JWT secret is required") return } if instance.Database.Type != "sqlite" && instance.Database.Type != "postgres" && instance.Database.Type != "mysql" { err = fmt.Errorf("invalid database type: %s (accepted values: sqlite, postgres, mysql)", instance.Database.Type) return } }) return instance, err } // Get returns the configuration instance (must be loaded first) func Get() *Config { if instance == nil { panic("configuration not loaded. Call Load() first") } return instance } // GetDSN returns the connection string based on the database type func (c *Config) GetDSN() string { switch c.Database.Type { case "sqlite": return c.Database.Path case "postgres": return fmt.Sprintf( "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", c.Database.Host, c.Database.Port, c.Database.User, c.Database.Password, c.Database.DBName, c.Database.SSLMode, ) case "mysql": return fmt.Sprintf( "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.Database.User, c.Database.Password, c.Database.Host, c.Database.Port, c.Database.DBName, ) default: return "" } } // GetDialector returns the GORM dialect name func (c *Config) GetDialector() string { return c.Database.Type } // FindConfigFile searches for the config file in multiple locations func FindConfigFile() (string, error) { locations := []string{ "./config.toml", "./config/config.toml", "/etc/oxspeak/config.toml", } // Add executable directory if exePath, err := os.Executable(); err == nil { exeDir := filepath.Dir(exePath) locations = append([]string{ filepath.Join(exeDir, "config.toml"), }, locations...) } for _, path := range locations { if _, err := os.Stat(path); err == nil { return path, nil } } return "", fmt.Errorf("configuration file not found in locations: %v", locations) }