162 lines
4.9 KiB
Go
162 lines
4.9 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) {
|
|
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
|
|
}
|