package config import ( "fmt" "net" "os" "strconv" "time" mysqlDriver "github.com/go-sql-driver/mysql" "gopkg.in/yaml.v3" ) type Config struct { Server ServerConfig `yaml:"server"` Database DatabaseConfig `yaml:"database"` Pricing PricingConfig `yaml:"pricing"` Export ExportConfig `yaml:"export"` Alerts AlertsConfig `yaml:"alerts"` Scheduler SchedulerConfig `yaml:"scheduler"` Notifications NotificationsConfig `yaml:"notifications"` Logging LoggingConfig `yaml:"logging"` } type ServerConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Mode string `yaml:"mode"` ReadTimeout time.Duration `yaml:"read_timeout"` WriteTimeout time.Duration `yaml:"write_timeout"` } type DatabaseConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Name string `yaml:"name"` User string `yaml:"user"` Password string `yaml:"password"` MaxOpenConns int `yaml:"max_open_conns"` MaxIdleConns int `yaml:"max_idle_conns"` ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime"` } func (d *DatabaseConfig) DSN() string { cfg := mysqlDriver.NewConfig() cfg.User = d.User cfg.Passwd = d.Password cfg.Net = "tcp" cfg.Addr = net.JoinHostPort(d.Host, strconv.Itoa(d.Port)) cfg.DBName = d.Name cfg.ParseTime = true cfg.Loc = time.Local cfg.Params = map[string]string{ "charset": "utf8mb4", } return cfg.FormatDSN() } type PricingConfig struct { DefaultMethod string `yaml:"default_method"` DefaultPeriodDays int `yaml:"default_period_days"` FreshnessGreenDays int `yaml:"freshness_green_days"` FreshnessYellowDays int `yaml:"freshness_yellow_days"` FreshnessRedDays int `yaml:"freshness_red_days"` MinQuotesForMedian int `yaml:"min_quotes_for_median"` PopularityDecayDays int `yaml:"popularity_decay_days"` } type ExportConfig struct { TempDir string `yaml:"temp_dir"` MaxFileAge time.Duration `yaml:"max_file_age"` CompanyName string `yaml:"company_name"` } type AlertsConfig struct { Enabled bool `yaml:"enabled"` CheckInterval time.Duration `yaml:"check_interval"` HighDemandThreshold int `yaml:"high_demand_threshold"` TrendingThresholdPercent int `yaml:"trending_threshold_percent"` } type SchedulerConfig struct { Enabled bool `yaml:"enabled"` PollInterval time.Duration `yaml:"poll_interval"` AlertsInterval time.Duration `yaml:"alerts_interval"` UpdatePricesInterval time.Duration `yaml:"update_prices_interval"` UpdatePopularityInterval time.Duration `yaml:"update_popularity_interval"` ResetWeeklyCountersInterval time.Duration `yaml:"reset_weekly_counters_interval"` ResetMonthlyCountersInterval time.Duration `yaml:"reset_monthly_counters_interval"` PartsLogBackfillInterval time.Duration `yaml:"parts_log_backfill_interval"` } type NotificationsConfig struct { EmailEnabled bool `yaml:"email_enabled"` SMTPHost string `yaml:"smtp_host"` SMTPPort int `yaml:"smtp_port"` SMTPUser string `yaml:"smtp_user"` SMTPPassword string `yaml:"smtp_password"` FromAddress string `yaml:"from_address"` } type LoggingConfig struct { Level string `yaml:"level"` Format string `yaml:"format"` Output string `yaml:"output"` FilePath string `yaml:"file_path"` } func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config file: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config file: %w", err) } cfg.setDefaults() return &cfg, nil } func (c *Config) setDefaults() { if c.Server.Host == "" { c.Server.Host = "127.0.0.1" } if c.Server.Port == 0 { c.Server.Port = 8084 } if c.Server.Mode == "" { c.Server.Mode = "release" } if c.Server.ReadTimeout == 0 { c.Server.ReadTimeout = 30 * time.Second } if c.Server.WriteTimeout == 0 { c.Server.WriteTimeout = 30 * time.Second } if c.Database.Port == 0 { c.Database.Port = 3306 } if c.Database.MaxOpenConns == 0 { c.Database.MaxOpenConns = 25 } if c.Database.MaxIdleConns == 0 { c.Database.MaxIdleConns = 5 } if c.Database.ConnMaxLifetime == 0 { c.Database.ConnMaxLifetime = 5 * time.Minute } if c.Pricing.DefaultMethod == "" { c.Pricing.DefaultMethod = "weighted_median" } if c.Pricing.DefaultPeriodDays == 0 { c.Pricing.DefaultPeriodDays = 90 } if c.Pricing.FreshnessGreenDays == 0 { c.Pricing.FreshnessGreenDays = 30 } if c.Pricing.FreshnessYellowDays == 0 { c.Pricing.FreshnessYellowDays = 60 } if c.Pricing.FreshnessRedDays == 0 { c.Pricing.FreshnessRedDays = 90 } if c.Pricing.MinQuotesForMedian == 0 { c.Pricing.MinQuotesForMedian = 3 } if c.Logging.Level == "" { c.Logging.Level = "info" } if c.Logging.Format == "" { c.Logging.Format = "json" } if c.Logging.Output == "" { c.Logging.Output = "stdout" } if c.Scheduler.PollInterval == 0 { c.Scheduler.PollInterval = 1 * time.Minute } if c.Alerts.CheckInterval == 0 { c.Alerts.CheckInterval = 1 * time.Hour } if c.Scheduler.AlertsInterval == 0 { c.Scheduler.AlertsInterval = c.Alerts.CheckInterval } if c.Scheduler.UpdatePricesInterval == 0 { c.Scheduler.UpdatePricesInterval = 24 * time.Hour } if c.Scheduler.UpdatePopularityInterval == 0 { c.Scheduler.UpdatePopularityInterval = 24 * time.Hour } if c.Scheduler.ResetWeeklyCountersInterval == 0 { c.Scheduler.ResetWeeklyCountersInterval = 7 * 24 * time.Hour } if c.Scheduler.ResetMonthlyCountersInterval == 0 { c.Scheduler.ResetMonthlyCountersInterval = 30 * 24 * time.Hour } if c.Scheduler.PartsLogBackfillInterval == 0 { c.Scheduler.PartsLogBackfillInterval = 24 * time.Hour } } func (c *Config) Address() string { return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) }