package config import ( "bytes" "errors" "fmt" "os" "gopkg.in/yaml.v3" ) type LoadOptions struct { Path string Units string Timezone string } func Load(opts LoadOptions) (Config, error) { cfg := Defaults() path := opts.Path if path == "" { path = DefaultPath } if err := mergeFile(&cfg, path); err != nil { if opts.Path != "" || !errors.Is(err, os.ErrNotExist) { return Config{}, err } } if opts.Units != "" { cfg.WeatherAPI.Units = opts.Units } if opts.Timezone != "" { cfg.WeatherAPI.Timezone = opts.Timezone } if err := normalizeReportModules(&cfg); err != nil { return Config{}, err } secrets, err := loadSecrets(cfg.Secrets) if err != nil { return Config{}, err } if err := Validate(cfg); err != nil { return Config{}, err } if err := applySecrets(secrets); err != nil { return Config{}, err } return cfg, nil } func LoadFile(path string) (Config, error) { return Load(LoadOptions{Path: path}) } func mergeFile(cfg *Config, path string) error { data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("read config %q: %w", path, err) } if err := rejectRetiredExecutionConfig(data); err != nil { return fmt.Errorf("parse config %q: %w", path, err) } decoder := yaml.NewDecoder(bytes.NewReader(data)) decoder.KnownFields(true) if err := decoder.Decode(cfg); err != nil { return fmt.Errorf("parse config %q: %w", path, err) } if cfg.MissingSource.Sources == nil { cfg.MissingSource.Sources = map[string]MissingSourcePolicy{} } if cfg.Reports == nil { cfg.Reports = map[string]ReportConfig{} } return nil } func rejectRetiredExecutionConfig(data []byte) error { var document yaml.Node if err := yaml.Unmarshal(data, &document); err != nil { return err } if len(document.Content) == 0 || document.Content[0].Kind != yaml.MappingNode { return nil } root := document.Content[0] for i := 0; i+1 < len(root.Content); i += 2 { if root.Content[i].Value == "scriptorium" { return fmt.Errorf("scriptorium configuration is no longer supported; migrate to promptkit configuration") } } return nil }