package config import ( "fmt" "os" "reflect" "strconv" "time" "github.com/subosito/gotenv" ) type ServerConfig struct { Host string `env:"SERVER_HOST"` Port int `env:"SERVER_PORT"` Timeout time.Duration `env:"SERVER_TIMEOUT"` ReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT"` WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT"` } type DatabaseConfig struct { MongoDB MongoConfig } type MongoConfig struct { URI string `env:"MONGODB_URI"` Name string `env:"MONGODB_NAME"` Timeout time.Duration `env:"MONGODB_TIMEOUT"` MaxPoolSize int `env:"MONGODB_MAX_POOL_SIZE"` } type CacheConfig struct { Redis RedisConfig } type RedisConfig struct { Host string `env:"REDIS_HOST"` Port int `env:"REDIS_PORT"` Password string `env:"REDIS_PASSWORD"` DB int `env:"REDIS_DB"` PoolSize int `env:"REDIS_POOL_SIZE"` } type LoggingConfig struct { Level string `env:"LOG_LEVEL"` Format string `env:"LOG_FORMAT"` Output string `env:"LOG_OUTPUT"` File LogFileConfig } type LogFileConfig struct { Path string `env:"LOG_FILE_PATH"` MaxSize int `env:"LOG_FILE_MAX_SIZE"` MaxBackups int `env:"LOG_FILE_MAX_BACKUPS"` MaxAge int `env:"LOG_FILE_MAX_AGE"` Compress bool `env:"LOG_FILE_COMPRESS"` } type AssetsConfig struct { FlagsPath string `env:"ASSETS_FLAGS_PATH"` } type RateLimitConfig struct { RequestsPerMinute int `env:"RATE_LIMIT_REQUESTS_PER_MINUTE"` Burst int `env:"RATE_LIMIT_BURST"` } type NotificationConfig struct { BaseURL string `env:"NOTIFICATION_BASE_URL"` Timeout time.Duration `env:"NOTIFICATION_TIMEOUT"` RetryAttempts int `env:"NOTIFICATION_RETRY_ATTEMPTS"` RetryDelay time.Duration `env:"NOTIFICATION_RETRY_DELAY"` EnableLogging bool `env:"NOTIFICATION_ENABLE_LOGGING"` UserAgent string `env:"NOTIFICATION_USER_AGENT"` } // OllamaConfig represents the configuration for the Ollama SDK type OllamaConfig struct { BaseURL string `env:"OLLAMA_BASE_URL" default:"http://localhost:11434"` Timeout time.Duration `env:"OLLAMA_TIMEOUT" default:"300s"` RetryAttempts int `env:"OLLAMA_RETRY_ATTEMPTS" default:"3"` RetryDelay time.Duration `env:"OLLAMA_RETRY_DELAY" default:"2s"` EnableLogging bool `env:"OLLAMA_ENABLE_LOGGING" default:"true"` UserAgent string `env:"OLLAMA_USER_AGENT" default:"TenderManagement-OllamaSDK/1.0"` DefaultModel string `env:"OLLAMA_DEFAULT_MODEL" default:"llama2"` DefaultTemperature float64 `env:"OLLAMA_DEFAULT_TEMPERATURE" default:"0.7"` DefaultTopP float64 `env:"OLLAMA_DEFAULT_TOP_P" default:"0.9"` DefaultMaxTokens int `env:"OLLAMA_DEFAULT_MAX_TOKENS" default:"1000"` EnableStreaming bool `env:"OLLAMA_ENABLE_STREAMING" default:"false"` } type HCaptchaConfig struct { SecretKey string `env:"HCAPTCHA_SECRET_KEY"` VerifyURL string `env:"HCAPTCHA_VERIFY_URL"` Timeout time.Duration `env:"HCAPTCHA_TIMEOUT"` } type ScraperConfig struct { BaseURL string `env:"SCRAPER_BASE_URL"` Timeout time.Duration `env:"SCRAPER_TIMEOUT"` } type MinIOConfig struct { Endpoint string `env:"MINIO_ENDPOINT" default:"localhost:9000"` AccessKeyID string `env:"MINIO_ACCESS_KEY_ID" default:""` SecretAccessKey string `env:"MINIO_SECRET_ACCESS_KEY" default:""` UseSSL bool `env:"MINIO_USE_SSL" default:"false"` Region string `env:"MINIO_REGION" default:"us-east-1"` DefaultBucket string `env:"MINIO_DEFAULT_BUCKET" default:"files"` HierarchicalBucket string `env:"MINIO_HIERARCHICAL_BUCKET" default:"documents"` } // LoadConfig loads configuration with priority: OS environment variables > .env file // OS environment variables take precedence over .env file values func LoadConfig[T any](path string, config T) (T, error) { // First, try to load .env file (this will not override existing OS env vars) envPath := fmt.Sprintf("%s/.env", path) // Check if .env file exists before trying to load it if _, err := os.Stat(envPath); err == nil { // Load .env file - gotenv.Load respects existing environment variables // and will not override them with values from the .env file if err := gotenv.Load(envPath); err != nil { return config, fmt.Errorf("failed to load .env file from %s: %w", envPath, err) } } // If .env file doesn't exist, we continue with OS environment variables only // Parse configuration using reflection // This will use OS env vars first, then fall back to values loaded from .env if err := parseConfig(config); err != nil { return config, fmt.Errorf("failed to parse configuration: %w", err) } return config, nil } // parseConfig uses reflection to parse environment variables into the config struct // Environment variables are read with the following priority: // 1. OS environment variables (highest priority) // 2. Values from .env file (loaded by gotenv.Load, lower priority) func parseConfig(config interface{}) error { v := reflect.ValueOf(config) if v.Kind() != reflect.Ptr { return fmt.Errorf("config must be a pointer") } v = v.Elem() if v.Kind() != reflect.Struct { return fmt.Errorf("config must be a struct") } return parseStruct(v) } // parseStruct recursively parses struct fields func parseStruct(v reflect.Value) error { t := v.Type() for i := 0; i < v.NumField(); i++ { field := v.Field(i) fieldType := t.Field(i) // Skip unexported fields if !field.CanSet() { continue } // Get env tag envTag := fieldType.Tag.Get("env") if envTag == "" { // If no env tag, try to parse nested struct if field.Kind() == reflect.Struct { if err := parseStruct(field); err != nil { return err } } continue } // Get environment variable value // os.Getenv will return OS env vars first, then values loaded from .env envValue := os.Getenv(envTag) if envValue == "" { continue } // Parse and set the field value if err := setFieldValue(field, envValue); err != nil { return fmt.Errorf("failed to set field %s (env: %s): %w", fieldType.Name, envTag, err) } } return nil } // setFieldValue sets the field value from environment variable string func setFieldValue(field reflect.Value, value string) error { switch field.Kind() { case reflect.String: field.SetString(value) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if field.Type() == reflect.TypeOf(time.Duration(0)) { // Handle time.Duration duration, err := time.ParseDuration(value) if err != nil { return fmt.Errorf("invalid duration format: %s", value) } field.Set(reflect.ValueOf(duration)) } else { // Handle regular integers intValue, err := strconv.ParseInt(value, 10, 64) if err != nil { return fmt.Errorf("invalid integer format: %s", value) } field.SetInt(intValue) } case reflect.Bool: boolValue, err := strconv.ParseBool(value) if err != nil { return fmt.Errorf("invalid boolean format: %s", value) } field.SetBool(boolValue) case reflect.Float32, reflect.Float64: floatValue, err := strconv.ParseFloat(value, 64) if err != nil { return fmt.Errorf("invalid float format: %s", value) } field.SetFloat(floatValue) default: return fmt.Errorf("unsupported field type: %s", field.Kind()) } return nil } // GetEnvWithPriority gets environment variable with explicit priority handling // Returns the value and a boolean indicating the source (true = OS env, false = .env file) func GetEnvWithPriority(key string) (string, bool) { // Check if the value exists in OS environment if value, exists := os.LookupEnv(key); exists { return value, true // From OS environment } // If not in OS environment, it might be from .env file if value := os.Getenv(key); value != "" { return value, false // From .env file } return "", false } // Helper function to get environment variable with default value func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // Helper function to get environment variable as int with default value func getEnvAsInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue } // Helper function to get environment variable as duration with default value func getEnvAsDuration(key string, defaultValue time.Duration) time.Duration { if value := os.Getenv(key); value != "" { if duration, err := time.ParseDuration(value); err == nil { return duration } } return defaultValue } // Helper function to get environment variable as bool with default value func getEnvAsBool(key string, defaultValue bool) bool { if value := os.Getenv(key); value != "" { if boolValue, err := strconv.ParseBool(value); err == nil { return boolValue } } return defaultValue }