internal/config: загрузка YAML+env (подстановка ${VAR:-default}, defaults, env override, Validate)
All checks were successful
build-test / build (push) Successful in 1m23s
All checks were successful
build-test / build (push) Successful in 1m23s
This commit is contained in:
135
internal/config/load.go
Normal file
135
internal/config/load.go
Normal file
@@ -0,0 +1,135 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user