// 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" ) const ( MissingSourceObservations = "observations" MissingSourceCurrent = "current" MissingSourceNarrative = "narrative" MissingSourceAlerts = "alerts" MissingSourceDiscussion = "discussion" MissingSourceWeatherStory = "weather_story" MissingSourceSPCConvectiveOutlooks = "spc_convective_outlooks" ) var supportedMissingSources = map[string]struct{}{ MissingSourceObservations: {}, MissingSourceCurrent: {}, MissingSourceNarrative: {}, MissingSourceAlerts: {}, MissingSourceDiscussion: {}, MissingSourceWeatherStory: {}, MissingSourceSPCConvectiveOutlooks: {}, } // IsSupportedMissingSource reports whether source accepts a missing-source // policy override. Required sources, including hourly, are not configurable. func IsSupportedMissingSource(source string) bool { _, ok := supportedMissingSources[source] return ok } type Config struct { WeatherAPI WeatherAPIConfig `yaml:"weather_api"` Location LocationConfig `yaml:"location"` Secrets SecretsConfig `yaml:"secrets"` Output OutputConfig `yaml:"output"` Notify NotifyConfig `yaml:"notify"` MissingSource MissingSourceConfig `yaml:"missing_source"` Promptkit PromptkitConfig `yaml:"promptkit"` Dayparts []DaypartConfig `yaml:"dayparts"` 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 OutputConfig 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 PromptkitConfig struct { Profile string `yaml:"profile"` ProfileFile string `yaml:"profile_file"` ProfileDir string `yaml:"profile_dir"` Timeout time.Duration `yaml:"timeout"` Local PromptkitLocalConfig `yaml:"local"` } type PromptkitLocalConfig struct { Endpoint string `yaml:"endpoint"` ConcurrencyLimit int `yaml:"concurrency_limit"` } type DaypartConfig struct { Name string `yaml:"name"` Start string `yaml:"start"` End string `yaml:"end"` } 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 || c.PathTemplates != nil } func (c ReportConfig) deterministicModulesConfigured() bool { return c.deterministicModulesSet || c.DeterministicModules != nil } 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") } }