// Package config owns application configuration structures, defaults, loading, // precedence, and validation. package config import ( "fmt" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gopkg.in/yaml.v3" ) type MissingSourcePolicy string type NotifyFailurePolicy string const ( MissingSourceError MissingSourcePolicy = "error" MissingSourceWarn MissingSourcePolicy = "warn" MissingSourceNone MissingSourcePolicy = "none" NotifyFailureError NotifyFailurePolicy = "error" ) type Config struct { WeatherAPI WeatherAPIConfig `yaml:"weather_api"` Location LocationConfig `yaml:"location"` Secrets SecretsConfig `yaml:"secrets"` Notify NotifyConfig `yaml:"notify"` MissingSource MissingSourceConfig `yaml:"missing_source"` Scriptorium ScriptoriumConfig `yaml:"scriptorium"` Workspace WorkspaceConfig `yaml:"workspace"` Dayparts []DaypartConfig `yaml:"dayparts"` RecentChange RecentChangeConfig `yaml:"recent_change"` Reports map[string]ReportConfig `yaml:"reports"` } type WeatherAPIConfig struct { BaseURL string `yaml:"base_url"` Timeout time.Duration `yaml:"timeout"` Precision int `yaml:"precision"` Units string `yaml:"units"` Timezone string `yaml:"timezone"` Format string `yaml:"format"` } type LocationConfig struct { ID string `yaml:"id"` Name string `yaml:"name"` Region string `yaml:"region"` } type SecretsConfig struct { Directory string `yaml:"directory"` } type NotifyConfig struct { Distributor DistributorNotifyConfig `yaml:"distributor"` } type DistributorNotifyConfig struct { Enabled bool `yaml:"enabled"` Endpoint string `yaml:"endpoint"` TokenEnv string `yaml:"token_env"` Timeout time.Duration `yaml:"timeout"` FailurePolicy NotifyFailurePolicy `yaml:"failure_policy"` PipelineIDTemplate string `yaml:"pipeline_id_template"` BundleIDTemplate string `yaml:"bundle_id_template"` IdempotencyKeyTemplate string `yaml:"idempotency_key_template"` Batch DistributorBatchNotifyConfig `yaml:"batch"` } type DistributorBatchNotifyConfig struct { Enabled bool `yaml:"enabled"` PipelineIDTemplate string `yaml:"pipeline_id_template"` BundleIDTemplate string `yaml:"bundle_id_template"` IdempotencyKeyTemplate string `yaml:"idempotency_key_template"` } type MissingSourceConfig struct { Default MissingSourcePolicy `yaml:"default"` Sources map[string]MissingSourcePolicy `yaml:"sources"` } type ScriptoriumConfig struct { Binary string `yaml:"binary"` ConfigPath string `yaml:"config_path"` Profile string `yaml:"profile"` Timeout time.Duration `yaml:"timeout"` ExtraArgs []string `yaml:"extra_args"` } type WorkspaceConfig struct { Root string `yaml:"root"` SnapshotsDir string `yaml:"snapshots_dir"` ReportsDir string `yaml:"reports_dir"` DataPackagesDir string `yaml:"data_packages_dir"` PreflightDir string `yaml:"preflight_dir"` NotificationsDir string `yaml:"notifications_dir"` } type DaypartConfig struct { Name string `yaml:"name"` Start string `yaml:"start"` End string `yaml:"end"` } type RecentChangeConfig struct { TemperatureDegrees float64 `yaml:"temperature_degrees"` PrecipProbabilityPoints int `yaml:"precip_probability_points"` WindGustMilesPerHour int `yaml:"wind_gust_miles_per_hour"` PrecipTimingShiftMinutes int `yaml:"precip_timing_shift_minutes"` } type ReportConfig struct { DeterministicModules []ModuleConfigItem `yaml:"deterministic_modules"` Distributor ReportDistributorConfig `yaml:"distributor"` deterministicModulesSet bool } type ReportDistributorConfig struct { PathTemplates []string `yaml:"path_templates"` pathTemplatesSet bool } type ModuleConfigItem struct { ID module.ID `yaml:"id"` Options any `yaml:"options,omitempty"` } func (c *ReportConfig) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { return fmt.Errorf("report entry must be a mapping") } for i := 0; i < len(value.Content); i += 2 { key := value.Content[i].Value node := value.Content[i+1] switch key { case "deterministic_modules": if err := node.Decode(&c.DeterministicModules); err != nil { return err } c.deterministicModulesSet = true case "distributor": if err := node.Decode(&c.Distributor); err != nil { return err } default: return fmt.Errorf("unknown report entry field %q", key) } } return nil } func (c *DistributorNotifyConfig) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { return fmt.Errorf("notify distributor entry must be a mapping") } for i := 0; i < len(value.Content); i += 2 { key := value.Content[i].Value node := value.Content[i+1] switch key { case "enabled": if err := node.Decode(&c.Enabled); err != nil { return err } case "endpoint": if err := node.Decode(&c.Endpoint); err != nil { return err } case "token_env": if err := node.Decode(&c.TokenEnv); err != nil { return err } case "timeout": if err := node.Decode(&c.Timeout); err != nil { return err } case "failure_policy": if err := node.Decode(&c.FailurePolicy); err != nil { return err } case "pipeline_id_template": if err := node.Decode(&c.PipelineIDTemplate); err != nil { return err } case "bundle_id_template": if err := node.Decode(&c.BundleIDTemplate); err != nil { return err } case "idempotency_key_template": if err := node.Decode(&c.IdempotencyKeyTemplate); err != nil { return err } case "batch": if err := node.Decode(&c.Batch); err != nil { return err } default: return fmt.Errorf("unknown notify distributor field %q", key) } } return nil } func (c *DistributorBatchNotifyConfig) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { return fmt.Errorf("notify distributor batch entry must be a mapping") } for i := 0; i < len(value.Content); i += 2 { key := value.Content[i].Value node := value.Content[i+1] switch key { case "enabled": if err := node.Decode(&c.Enabled); err != nil { return err } case "pipeline_id_template": if err := node.Decode(&c.PipelineIDTemplate); err != nil { return err } case "bundle_id_template": if err := node.Decode(&c.BundleIDTemplate); err != nil { return err } case "idempotency_key_template": if err := node.Decode(&c.IdempotencyKeyTemplate); err != nil { return err } default: return fmt.Errorf("unknown notify distributor batch field %q", key) } } return nil } func (c *ReportDistributorConfig) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { return fmt.Errorf("report distributor entry must be a mapping") } for i := 0; i < len(value.Content); i += 2 { key := value.Content[i].Value node := value.Content[i+1] switch key { case "path_templates": if err := node.Decode(&c.PathTemplates); err != nil { return err } c.pathTemplatesSet = true default: return fmt.Errorf("unknown report distributor field %q", key) } } return nil } func (c ReportDistributorConfig) PathTemplatesSet() bool { return c.pathTemplatesSet } func (m *ModuleConfigItem) UnmarshalYAML(value *yaml.Node) error { switch value.Kind { case yaml.ScalarNode: if value.Value == "" { return fmt.Errorf("module id is required") } m.ID = module.ID(value.Value) return nil case yaml.MappingNode: var sawID bool for i := 0; i < len(value.Content); i += 2 { key := value.Content[i].Value node := value.Content[i+1] switch key { case "id": if err := node.Decode(&m.ID); err != nil { return err } sawID = true case "options": var options any if err := node.Decode(&options); err != nil { return err } m.Options = options default: return fmt.Errorf("unknown module entry field %q", key) } } if !sawID || m.ID == "" { return fmt.Errorf("module id is required") } return nil default: return fmt.Errorf("module entry must be a string or mapping") } }