Files
2025-11-03 12:24:01 +02:00

209 lines
4.6 KiB
Go

// internal/config/config.go
package config
import (
"fmt"
"os"
"path/filepath"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
SMTP SMTPConfig
JWT JWTConfig
}
type ServerConfig struct {
Port string
Env string
}
type DatabaseConfig struct {
Host string
Port string
User string
Password string
Name string
SSLMode string
}
type RedisConfig struct {
Addr string
Password string
DB int
}
type SMTPConfig struct {
Host string
Port int
Username string
Password string
From string
}
type JWTConfig struct {
Secret string
PublicKey string
Expiry string
}
// Package-level state for a single, watched config instance
var (
vip *viper.Viper
cfgCache *Config
cfgMu sync.RWMutex
initOnce sync.Once
initErr error
listeners []func(*Config)
listenersMu sync.RWMutex
)
// LoadConfig returns the current configuration. On first call it initializes
// Viper, loads the config from a deterministic location, and starts watching
// for changes. Subsequent calls are cheap and thread-safe.
func LoadConfig() (*Config, error) {
initOnce.Do(func() {
vip = viper.New()
configureViper(vip)
// Initial read must succeed; later changes are handled via WatchConfig
if err := vip.ReadInConfig(); err != nil {
initErr = fmt.Errorf("failed to load config: %w", err)
return
}
// Build cache from initial values
cfg := buildFromViper(vip)
cfgMu.Lock()
cfgCache = cfg
cfgMu.Unlock()
// Watch for changes and update cache atomically
vip.WatchConfig()
vip.OnConfigChange(func(_ fsnotify.Event) {
// viper already re-reads on change; rebuild view and swap
newCfg := buildFromViper(vip)
cfgMu.Lock()
cfgCache = newCfg
cfgMu.Unlock()
notifyListeners(newCfg)
})
})
if initErr != nil {
return nil, initErr
}
cfgMu.RLock()
defer cfgMu.RUnlock()
return cloneConfig(cfgCache), nil
}
// DSN returns the Data Source Name for PostgreSQL connection
func (db DatabaseConfig) DSN() string {
return fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s?sslmode=%s",
db.User, db.Password, db.Host, db.Port, db.Name, db.SSLMode,
)
}
// RegisterChangeListener registers a callback invoked whenever the configuration
// file changes and the in-memory configuration is updated.
func RegisterChangeListener(listener func(*Config)) {
listenersMu.Lock()
defer listenersMu.Unlock()
listeners = append(listeners, listener)
}
// --- internal helpers ---
func configureViper(v *viper.Viper) {
// Allow env vars to override any key (expects exact key names used below)
v.AutomaticEnv()
if path, ok := resolveConfigFilePath(); ok {
v.SetConfigFile(path)
return
}
// Fallback to looking for a .env in stable, nearby locations.
v.SetConfigName(".env")
v.SetConfigType("env")
v.AddConfigPath(".")
v.AddConfigPath("..")
}
func resolveConfigFilePath() (string, bool) {
if p := os.Getenv("CONFIG_FILE"); p != "" {
return p, true
}
cwd, _ := os.Getwd()
candidates := []string{
filepath.Join(cwd, ".env"),
filepath.Join(cwd, "go-server", ".env"),
filepath.Join(cwd, "..", ".env"),
filepath.Join(cwd, "..", "go-server", ".env"),
}
for _, c := range candidates {
if info, err := os.Stat(c); err == nil && !info.IsDir() {
return c, true
}
}
return "", false
}
func buildFromViper(v *viper.Viper) *Config {
return &Config{
Server: ServerConfig{
Port: v.GetString("SERVER_PORT"),
Env: v.GetString("SERVER_ENV"),
},
Database: DatabaseConfig{
Host: v.GetString("DB_HOST"),
Port: v.GetString("DB_PORT"),
User: v.GetString("DB_USER"),
Password: v.GetString("DB_PASSWORD"),
Name: v.GetString("DB_NAME"),
SSLMode: v.GetString("DB_SSL_MODE"),
},
Redis: RedisConfig{
Addr: v.GetString("REDIS_ADDR"),
Password: v.GetString("REDIS_PASSWORD"),
DB: v.GetInt("REDIS_DB"),
},
SMTP: SMTPConfig{
Host: v.GetString("SMTP_HOST"),
Port: v.GetInt("SMTP_PORT"),
Username: v.GetString("SMTP_USERNAME"),
Password: v.GetString("SMTP_PASSWORD"),
From: v.GetString("SMTP_FROM"),
},
JWT: JWTConfig{
Secret: v.GetString("JWT_SECRET"),
Expiry: v.GetString("JWT_EXPIRY"),
PublicKey: v.GetString("JWT_PUBLIC_KEY"),
},
}
}
func cloneConfig(c *Config) *Config {
if c == nil {
return nil
}
cc := *c
return &cc
}
func notifyListeners(c *Config) {
listenersMu.RLock()
defer listenersMu.RUnlock()
for _, l := range listeners {
// Call listeners in-goroutine to avoid blocking the watcher
go l(cloneConfig(c))
}
}