package config import ( "fmt" "os" "reflect" "strconv" "strings" ) // decodeValue populates the struct pointed to by v. Precedence, highest first: // process environment variable, the parsed .env file, then the default tag. func decodeValue(v reflect.Value, fromEnv map[string]string) error { if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { return fmt.Errorf("config: decode expects a pointer to a struct") } return decodeStruct(v.Elem(), fromEnv) } func decodeStruct(sv reflect.Value, fromEnv map[string]string) error { st := sv.Type() for i := 0; i < st.NumField(); i++ { field := st.Field(i) if !field.IsExported() { continue } fv := sv.Field(i) envTag, hasEnv := field.Tag.Lookup("env") if !hasEnv { // Untagged struct fields are nested config groups. if fv.Kind() == reflect.Struct { if err := decodeStruct(fv, fromEnv); err != nil { return err } } continue } name := envTagName(envTag) raw := lookupRaw(name, field.Tag.Get("default"), fromEnv) if err := setField(fv, raw); err != nil { return fmt.Errorf("config: %s: %w", name, err) } } return nil } // envTagName extracts the variable name from an env tag (dropping any // comma-separated options that may be added later). func envTagName(tag string) string { name, _, _ := strings.Cut(tag, ",") return strings.TrimSpace(name) } // lookupRaw returns the effective raw value for key: process env, then .env, // then default. func lookupRaw(key, def string, fromEnv map[string]string) string { if v, ok := os.LookupEnv(key); ok { return v } if v, ok := fromEnv[key]; ok { return v } return def } func setField(fv reflect.Value, raw string) error { switch fv.Kind() { case reflect.String: fv.SetString(raw) return nil case reflect.Bool: return setBool(fv, raw) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return setInt(fv, raw) case reflect.Slice: return setStringSlice(fv, raw) default: return fmt.Errorf("unsupported kind %s", fv.Kind()) } } func setBool(fv reflect.Value, raw string) error { if raw == "" { fv.SetBool(false) return nil } b, err := strconv.ParseBool(strings.ToLower(raw)) if err != nil { return fmt.Errorf("invalid boolean %q", raw) } fv.SetBool(b) return nil } func setInt(fv reflect.Value, raw string) error { if raw == "" { fv.SetInt(0) return nil } n, err := strconv.ParseInt(raw, 10, 64) if err != nil { return fmt.Errorf("invalid integer %q", raw) } fv.SetInt(n) return nil } func setStringSlice(fv reflect.Value, raw string) error { if fv.Type().Elem().Kind() != reflect.String { return fmt.Errorf("unsupported slice element kind %s", fv.Type().Elem().Kind()) } if strings.TrimSpace(raw) == "" { fv.Set(reflect.MakeSlice(fv.Type(), 0, 0)) return nil } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if t := strings.TrimSpace(p); t != "" { out = append(out, t) } } fv.Set(reflect.ValueOf(out)) return nil }