135 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|
|
} |