package config import ( "fmt" "os" "reflect" "strings" "gopkg.in/yaml.v3" ) // Load читает конфиг из yaml-файла, подставляет ${VAR:-default}, накладывает // env-переменные (тег env) и валидирует. Если path пустой — ищет config.yaml // в CWD. Ошибка C4 если файл не существует (но не fatal при path=""). func Load(path string) (*Config, error) { noFile := false if path == "" { path = "config.yaml" } raw, err := os.ReadFile(path) if err != nil { if strings.Contains(err.Error(), "no such file") || strings.Contains(err.Error(), "cannot find") { if path == "config.yaml" { // Файл не указан и не найден — не фатально, соберём дефолты+env noFile = true } else { return nil, fmt.Errorf("%w: %s", ErrFileNotFound, path) } } else { return nil, fmt.Errorf("%w: read %s: %v", ErrInvalidFormat, path, err) } } cfg := &Config{} if !noFile { expanded := os.Expand(string(raw), envLookup) if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil { return nil, fmt.Errorf("%w: parse %s: %v", ErrInvalidFormat, path, err) } } // дефолты для zero-значений applyDefaults(cfg) // env override (только если env-переменная непустая) applyEnvOverrides(cfg) if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil } // envLookup обрабатывает ${VAR:-default} в YAML. func envLookup(key string) string { parts := strings.SplitN(key, ":-", 2) v := os.Getenv(parts[0]) if v != "" { return v } if len(parts) > 1 { return parts[1] } return "" } // ---- reflect-based defaults и env override ---- type fieldMeta struct { defaultVal string envKey string } func collectMeta(f reflect.StructField) fieldMeta { return fieldMeta{ envKey: f.Tag.Get("env"), defaultVal: f.Tag.Get("default"), } } // applyDefaults проставляет default-значения для zero-полей. func applyDefaults(cfg *Config) { walk(reflect.ValueOf(cfg).Elem(), func(fv reflect.Value, meta fieldMeta) { if meta.defaultVal == "" { return } // только zero-поля if !fv.IsZero() { return } // Duration требует ручного Unmarshal if dur, ok := fv.Addr().Interface().(*Duration); ok { _ = dur.UnmarshalYAML(func(v any) error { *(v.(*string)) = meta.defaultVal return nil }) return } // строка if fv.Kind() == reflect.String { fv.SetString(meta.defaultVal) } }) } // applyEnvOverrides перезаписывает поля из env-переменных, если они непустые. func applyEnvOverrides(cfg *Config) { walk(reflect.ValueOf(cfg).Elem(), func(fv reflect.Value, meta fieldMeta) { if meta.envKey == "" { return } envVal := os.Getenv(meta.envKey) if envVal == "" { return } if dur, ok := fv.Addr().Interface().(*Duration); ok { _ = dur.UnmarshalYAML(func(v any) error { *(v.(*string)) = envVal return nil }) return } if fv.Kind() == reflect.String { fv.SetString(envVal) } }) } // walk рекурсивно обходит все leaf-поля структуры (string, Duration) и зовёт fn. func walk(v reflect.Value, fn func(reflect.Value, fieldMeta)) { t := v.Type() durType := reflect.TypeOf(Duration(0)) for i := range t.NumField() { f := t.Field(i) fv := v.Field(i) if !fv.CanSet() { continue } if fv.Kind() == reflect.String { fn(fv, collectMeta(f)) } else if fv.Type() == durType { fn(fv, collectMeta(f)) } else if fv.Kind() == reflect.Struct { walk(fv, fn) } } }