Files
weatherreporter/internal/config/reports.go

138 lines
4.1 KiB
Go

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) {
return traverseReportModules(&cfg, reportModuleTraversalOptions{
normalizeOptions: true,
})
}
func normalizeReportModules(cfg *Config) error {
if cfg.Reports == nil {
cfg.Reports = map[string]ReportConfig{}
}
_, err := traverseReportModules(cfg, reportModuleTraversalOptions{
normalizeOptions: true,
updateConfig: true,
})
return err
}
func validateReportModules(cfg Config) error {
_, err := traverseReportModules(&cfg, reportModuleTraversalOptions{
normalizeOptions: true,
})
return err
}
type reportModuleTraversalOptions struct {
normalizeOptions bool
updateConfig bool
}
func traverseReportModules(cfg *Config, opts reportModuleTraversalOptions) (map[report.ID][]module.ConfigItem, error) {
overrides := map[report.ID][]module.ConfigItem{}
if cfg.Reports == nil {
return overrides, nil
}
moduleRegistry, err := briefing.DefaultModuleRegistry()
if err != nil {
return nil, 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 nil, fmt.Errorf("reports.%s: %w", key, err)
}
if previous, ok := seenReports[reportID]; ok {
return nil, fmt.Errorf("reports.%s duplicates report override %q", key, previous)
}
seenReports[reportID] = key
if _, err := reportRegistry.Lookup(reportID); err != nil {
return nil, fmt.Errorf("reports.%s: %w", key, err)
}
if !reportCfg.deterministicModulesSet {
continue
}
items, normalized, err := moduleItemsFromConfig(moduleRegistry, key, reportCfg.DeterministicModules, opts.normalizeOptions)
if err != nil {
return nil, err
}
if err := moduleRegistry.ValidateComposition(reportID, items); err != nil {
return nil, fmt.Errorf("reports.%s.deterministic_modules: %w", key, err)
}
overrides[reportID] = items
if opts.updateConfig {
reportCfg.DeterministicModules = normalized
cfg.Reports[key] = reportCfg
}
}
return overrides, nil
}
func moduleItemsFromConfig(registry briefing.ModuleRegistry, reportKey string, items []ModuleConfigItem, normalizeOptions bool) ([]module.ConfigItem, []ModuleConfigItem, error) {
out := make([]module.ConfigItem, 0, len(items))
normalizedItems := append([]ModuleConfigItem(nil), items...)
for i, item := range items {
options := item.Options
if normalizeOptions {
var err error
options, err = normalizeModuleOptions(registry, item.ID, item.Options)
if err != nil {
return nil, nil, fmt.Errorf("reports.%s.deterministic_modules[%d]: %w", reportKey, i, err)
}
normalizedItems[i].Options = options
}
out = append(out, module.ConfigItem{ID: item.ID, Options: options})
}
return out, normalizedItems, 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)
}
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
}