package config import ( "fmt" "os" "strings" "gopkg.in/yaml.v3" ) // Config holds weatherapi datasource configuration. type Config struct { Databases []DatabaseConfig } type DatabaseConfig struct { Name string `yaml:"name"` Driver string `yaml:"driver"` Params DatabaseParams `yaml:"params"` } type DatabaseParams struct { URI string `yaml:"uri"` Username string `yaml:"username"` Password string `yaml:"password"` } type configWrapper struct { Databases []DatabaseConfig `yaml:"databases"` } func Load(path string) (*Config, error) { if strings.TrimSpace(path) == "" { path = "config.yml" } raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("config.Load: read %q: %w", path, err) } var list []DatabaseConfig if err := decodeStrict(raw, &list); err == nil && len(list) > 0 { cfg := &Config{Databases: list} if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil } var wrapped configWrapper if err := decodeStrict(raw, &wrapped); err != nil { return nil, fmt.Errorf("config.Load: parse YAML %q: %w", path, err) } cfg := &Config{Databases: wrapped.Databases} if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil } func decodeStrict(raw []byte, out any) error { dec := yaml.NewDecoder(strings.NewReader(string(raw))) dec.KnownFields(true) if err := dec.Decode(out); err != nil { return err } var extra any if err := dec.Decode(&extra); err == nil { return fmt.Errorf("contains multiple YAML documents; expected exactly one") } return nil } func (c *Config) Validate() error { if c == nil { return fmt.Errorf("config validation failed: config is nil") } if len(c.Databases) == 0 { return fmt.Errorf("config validation failed: no databases configured") } seen := map[string]struct{}{} for i, db := range c.Databases { path := fmt.Sprintf("databases[%d]", i) if strings.TrimSpace(db.Name) == "" { return fmt.Errorf("config validation failed: %s.name is required", path) } if _, ok := seen[db.Name]; ok { return fmt.Errorf("config validation failed: %s.name %q is duplicated", path, db.Name) } seen[db.Name] = struct{}{} if strings.TrimSpace(db.Driver) == "" { return fmt.Errorf("config validation failed: %s.driver is required", path) } if strings.TrimSpace(db.Params.URI) == "" { return fmt.Errorf("config validation failed: %s.params.uri is required", path) } if strings.TrimSpace(db.Params.Username) == "" { return fmt.Errorf("config validation failed: %s.params.username is required", path) } if strings.TrimSpace(db.Params.Password) == "" { return fmt.Errorf("config validation failed: %s.params.password is required", path) } } return nil } func (c *Config) FindDatabase(name string) (DatabaseConfig, bool) { for _, db := range c.Databases { if db.Name == name { return db, true } } return DatabaseConfig{}, false }