Files
weatherreporter/internal/config/reports.go

128 lines
3.8 KiB
Go

package config
import (
"bytes"
"fmt"
"reflect"
"strings"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gopkg.in/yaml.v3"
)
func (cfg Config) ReportModuleOverrides() map[report.ID][]module.ConfigItem {
overrides := map[report.ID][]module.ConfigItem{}
for key, reportCfg := range cfg.Reports {
if !reportCfg.deterministicModulesSet {
continue
}
id, err := reportIDForConfigKey(key)
if err != nil {
continue
}
items := make([]module.ConfigItem, 0, len(reportCfg.DeterministicModules))
for _, item := range reportCfg.DeterministicModules {
items = append(items, module.ConfigItem{ID: item.ID, Options: item.Options})
}
overrides[id] = items
}
return overrides
}
func normalizeReportModules(cfg *Config) error {
if cfg.Reports == nil {
cfg.Reports = map[string]ReportConfig{}
}
moduleRegistry, err := briefing.DefaultModuleRegistry()
if err != nil {
return fmt.Errorf("initialize module registry: %w", err)
}
reportRegistry := report.DefaultRegistry()
seenReports := map[report.ID]string{}
for key, reportCfg := range cfg.Reports {
reportID, err := reportIDForConfigKey(key)
if err != nil {
return err
}
if previous, ok := seenReports[reportID]; ok {
return fmt.Errorf("reports.%s duplicates report override %q", key, previous)
}
seenReports[reportID] = key
if _, err := reportRegistry.Lookup(reportID); err != nil {
return fmt.Errorf("reports.%s: %w", key, err)
}
if !reportCfg.deterministicModulesSet {
continue
}
items := make([]module.ConfigItem, 0, len(reportCfg.DeterministicModules))
for i, rawItem := range reportCfg.DeterministicModules {
options, err := normalizeModuleOptions(moduleRegistry, rawItem.ID, rawItem.Options)
if err != nil {
return fmt.Errorf("reports.%s.deterministic_modules[%d]: %w", key, i, err)
}
reportCfg.DeterministicModules[i].Options = options
items = append(items, module.ConfigItem{ID: rawItem.ID, Options: options})
}
if err := moduleRegistry.ValidateComposition(reportID, items); err != nil {
return fmt.Errorf("reports.%s.deterministic_modules: %w", key, err)
}
cfg.Reports[key] = reportCfg
}
return nil
}
func normalizeModuleOptions(registry briefing.ModuleRegistry, id module.ID, raw any) (any, error) {
if raw == nil {
return nil, nil
}
definition, err := registry.Lookup(id)
if err != nil {
return nil, err
}
if definition.DefaultOptions == nil {
return nil, fmt.Errorf("module %q does not accept options", id)
}
optionType := reflect.TypeOf(definition.DefaultOptions)
normalized, err := decodeKnownOptions(raw, optionType)
if err != nil {
return nil, fmt.Errorf("module %q options are invalid: %w", id, err)
}
return normalized, nil
}
func decodeKnownOptions(raw any, optionType reflect.Type) (any, error) {
data, err := yaml.Marshal(raw)
if err != nil {
return nil, err
}
target := reflect.New(optionType)
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)
if err := decoder.Decode(target.Interface()); err != nil {
return nil, err
}
return target.Elem().Interface(), nil
}
func reportIDForConfigKey(key string) (report.ID, error) {
normalized := strings.ReplaceAll(strings.TrimSpace(strings.ToLower(key)), "-", "_")
switch normalized {
case "daily", "daily_today":
return report.DailyToday, nil
case "tomorrow", "daily_tomorrow":
return report.DailyTomorrow, nil
case "hourly":
return report.Hourly, nil
case "three_day", "three_day_outlook":
return report.ThreeDay, nil
case "weekend", "weekend_outlook":
return report.Weekend, nil
case "storm", "storm_report":
return report.Storm, nil
default:
return "", fmt.Errorf("reports.%s is not a known report", key)
}
}