package config import ( "fmt" "reflect" "strconv" "strings" "time" ) // durationType is compared against to tell a Duration from a plain int64. var durationType = reflect.TypeOf(time.Duration(0)) // ApplyEnv overwrites every field carrying an `env` tag whose variable is set // to a non-empty value. lookup is usually os.Getenv; tests pass a fake. // // Supported kinds: string, bool, all ints, time.Duration and []string // (comma-separated, whitespace trimmed, empty elements dropped). func ApplyEnv(cfg *Config, lookup func(string) string) error { return applyEnv(reflect.ValueOf(cfg).Elem(), lookup) } func applyEnv(v reflect.Value, lookup func(string) string) error { t := v.Type() for i := range t.NumField() { field := t.Field(i) if !field.IsExported() { continue } fv := v.Field(i) if fv.Kind() == reflect.Struct && field.Type != durationType { if err := applyEnv(fv, lookup); err != nil { return err } continue } name := field.Tag.Get("env") if name == "" { continue } raw := strings.TrimSpace(lookup(name)) if raw == "" { continue } if err := setFromString(fv, raw); err != nil { return fmt.Errorf("%s: %w", name, err) } } return nil } func setFromString(fv reflect.Value, raw string) error { if fv.Type() == durationType { d, err := time.ParseDuration(raw) if err != nil { return fmt.Errorf("invalid duration %q", raw) } fv.SetInt(int64(d)) return nil } switch fv.Kind() { case reflect.String: fv.SetString(raw) case reflect.Bool: b, err := strconv.ParseBool(raw) if err != nil { return fmt.Errorf("invalid boolean %q (want true or false)", raw) } fv.SetBool(b) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(raw, 10, 64) if err != nil { return fmt.Errorf("invalid integer %q", raw) } fv.SetInt(n) case reflect.Slice: if fv.Type().Elem().Kind() != reflect.String { return fmt.Errorf("unsupported slice type %s", fv.Type()) } fv.Set(reflect.ValueOf(splitList(raw))) default: return fmt.Errorf("unsupported field type %s", fv.Type()) } return nil } // splitList parses a comma-separated env value. A lone "-" clears the list, // which is the only way to turn a YAML list back into an empty one from the // environment. func splitList(raw string) []string { if raw == "-" { return []string{} } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } return out }