package config import ( "fmt" "net/url" "strconv" "strings" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func Validate(cfg Config) error { if cfg.WeatherAPI.BaseURL != "" { parsed, err := url.Parse(cfg.WeatherAPI.BaseURL) if err != nil || parsed.Scheme == "" || parsed.Host == "" { return fmt.Errorf("weather_api.base_url must be an absolute URL") } } if cfg.WeatherAPI.Timeout <= 0 { return fmt.Errorf("weather_api.timeout must be greater than zero") } if cfg.WeatherAPI.Precision < 0 { return fmt.Errorf("weather_api.precision must be zero or greater") } if cfg.WeatherAPI.Units == "" { return fmt.Errorf("weather_api.units is required") } if cfg.WeatherAPI.Timezone == "" { return fmt.Errorf("weather_api.timezone is required") } if _, err := timeutil.LoadLocation(cfg.WeatherAPI.Timezone); err != nil { return fmt.Errorf("weather_api.timezone %q is invalid: %w", cfg.WeatherAPI.Timezone, err) } if cfg.WeatherAPI.Format == "" { return fmt.Errorf("weather_api.format is required") } if cfg.WeatherAPI.Format != "json" { return fmt.Errorf("weather_api.format must be json") } if err := validatePolicy("missing_source.default", cfg.MissingSource.Default); err != nil { return err } for source, policy := range cfg.MissingSource.Sources { if strings.TrimSpace(source) == "" { return fmt.Errorf("missing_source.sources contains an empty source name") } if err := validatePolicy("missing_source.sources."+source, policy); err != nil { return err } } if cfg.Scriptorium.Binary == "" { return fmt.Errorf("scriptorium.binary is required") } if cfg.Scriptorium.Timeout <= 0 { return fmt.Errorf("scriptorium.timeout must be greater than zero") } if cfg.Workspace.Root == "" { return fmt.Errorf("workspace.root is required") } if cfg.Reports.OutputDir == "" { return fmt.Errorf("reports.output_dir is required") } if len(cfg.Dayparts) == 0 { return fmt.Errorf("dayparts must contain at least one entry") } for i, daypart := range cfg.Dayparts { if strings.TrimSpace(daypart.Name) == "" { return fmt.Errorf("dayparts[%d].name is required", i) } if err := validateClockTime(daypart.Start); err != nil { return fmt.Errorf("dayparts[%d].start is invalid: %w", i, err) } if err := validateClockTime(daypart.End); err != nil { return fmt.Errorf("dayparts[%d].end is invalid: %w", i, err) } } return nil } func validatePolicy(name string, policy MissingSourcePolicy) error { switch policy { case MissingSourceError, MissingSourceWarn, MissingSourceNone: return nil default: return fmt.Errorf("%s must be one of error, warn, or none", name) } } func validateClockTime(value string) error { parts := strings.Split(value, ":") if len(parts) != 2 { return fmt.Errorf("expected HH:MM") } hour, err := strconv.Atoi(parts[0]) if err != nil { return fmt.Errorf("invalid hour") } minute, err := strconv.Atoi(parts[1]) if err != nil { return fmt.Errorf("invalid minute") } if hour < 0 || hour > 24 { return fmt.Errorf("hour must be between 00 and 24") } if minute < 0 || minute > 59 { return fmt.Errorf("minute must be between 00 and 59") } if hour == 24 && minute != 0 { return fmt.Errorf("24 is only valid as 24:00") } return nil }