Files
ratatoskr-go/internal/config/load.go
2026-08-14 22:59:23 +05:00

135 lines
3.5 KiB
Go

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) {
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") {
return nil, fmt.Errorf("%w: %s", ErrFileNotFound, path)
}
return nil, fmt.Errorf("%w: read %s: %v", ErrInvalidFormat, path, err)
}
// подстановка ${VAR:-default}
expanded := os.Expand(string(raw), envLookup)
cfg := &Config{}
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)
}
}
}