206 lines
6.4 KiB
Go
206 lines
6.4 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 (cfg Config) ReportDistributorPathOverrides() (map[report.ID][]string, error) {
|
|
return traverseReportDistributorPathOverrides(cfg)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func validateReportDistributorPathOverrides(cfg Config) error {
|
|
_, err := traverseReportDistributorPathOverrides(cfg)
|
|
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.deterministicModulesConfigured() {
|
|
continue
|
|
}
|
|
if len(reportCfg.DeterministicModules) == 0 {
|
|
return nil, fmt.Errorf("reports.%s.deterministic_modules must contain at least one entry", key)
|
|
}
|
|
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 traverseReportDistributorPathOverrides(cfg Config) (map[report.ID][]string, error) {
|
|
overrides := map[report.ID][]string{}
|
|
if cfg.Reports == nil {
|
|
return overrides, nil
|
|
}
|
|
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.Distributor.PathTemplatesSet() {
|
|
continue
|
|
}
|
|
if err := validateReportDistributorPathTemplates(key, reportID, reportCfg.Distributor.PathTemplates); err != nil {
|
|
return nil, err
|
|
}
|
|
overrides[reportID] = append([]string(nil), reportCfg.Distributor.PathTemplates...)
|
|
}
|
|
return overrides, nil
|
|
}
|
|
|
|
func validateReportDistributorPathTemplates(reportKey string, reportID report.ID, templates []string) error {
|
|
name := fmt.Sprintf("reports.%s.distributor.path_templates", reportKey)
|
|
_, err := RenderDistributorReportPaths(name, templates, sampleDistributorTemplateValuesForReport(reportID))
|
|
return err
|
|
}
|
|
|
|
func sampleDistributorTemplateValues() DistributorTemplateValues {
|
|
return sampleDistributorTemplateValuesForReport(report.Daily)
|
|
}
|
|
|
|
func sampleDistributorTemplateValuesForReport(_ report.ID) DistributorTemplateValues {
|
|
return DistributorTemplateValues{
|
|
LocationID: "location",
|
|
ReportID: "report",
|
|
RunID: "run",
|
|
ArtifactGroup: "artifact",
|
|
BatchOutputName: "report.md",
|
|
ValidStartDate: "2026-05-29",
|
|
ValidEndDate: "2026-05-30",
|
|
ValidStartTime: "0000",
|
|
ValidEndTime: "0000",
|
|
ValidStartStamp: "2026-05-29T0000",
|
|
ValidEndStamp: "2026-05-30T0000",
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|