package config import ( "bytes" "fmt" "reflect" "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, error) { overrides := map[report.ID][]module.ConfigItem{} seenReports := map[report.ID]string{} for key, reportCfg := range cfg.Reports { id, err := report.IDForConfigKey(key) if err != nil { return nil, fmt.Errorf("reports.%s: %w", key, err) } if previous, ok := seenReports[id]; ok { return nil, fmt.Errorf("reports.%s duplicates report override %q", key, previous) } seenReports[id] = key if !reportCfg.deterministicModulesSet { continue } overrides[id] = moduleItemsFromConfig(reportCfg.DeterministicModules) } return overrides, nil } 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 := report.IDForConfigKey(key) if err != nil { return fmt.Errorf("reports.%s: %w", key, 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 } 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 := moduleItemsFromConfig(reportCfg.DeterministicModules) 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 validateReportModules(cfg Config) error { if cfg.Reports == nil { return nil } 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 := report.IDForConfigKey(key) if err != nil { return fmt.Errorf("reports.%s: %w", key, 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 := rawItem.Options if options != nil { normalized, err := normalizeModuleOptions(moduleRegistry, rawItem.ID, options) if err != nil { return fmt.Errorf("reports.%s.deterministic_modules[%d]: %w", key, i, err) } options = normalized } 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) } } return nil } func moduleItemsFromConfig(items []ModuleConfigItem) []module.ConfigItem { out := make([]module.ConfigItem, 0, len(items)) for _, item := range items { out = append(out, module.ConfigItem{ID: item.ID, Options: item.Options}) } return out } 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) } if err := definition.ValidateOptions(raw); err == nil { return raw, nil } 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 }