package envstruct // import "go.vedthiru.com/envstruct" import ( "fmt" "os" "reflect" "strconv" "strings" "unicode" ) func convertCase(prefix string, s string) string { var envVarBuf strings.Builder if prefix != "" { envVarBuf.WriteString(prefix) } skip := false for i, c := range s { u := unicode.IsUpper(c) if i == 0 { skip = u } else if !skip && u { envVarBuf.WriteRune('_') skip = true } else if skip && !u { skip = false } envVarBuf.WriteRune(unicode.ToUpper(c)) } return envVarBuf.String() } // Initializes s from environment variables. s must be a pointer to a struct // (panics otherwise). prefix may be empty, though it is recommended you set it // so that your environment variables do not clash with others. // // Automatically converts struct field names to environment variable case, using // the following algorithm: non-consecutive uppercase letters in the field name // are preceded by an underscore. Example: the value for field UseJSON is read // from USE_JSON. Nested structs are handled similarly: the field name is added // on as the struct tree is traversed. Example: if UseJSON is inside a struct in // field Log, the value is read from LOG_USE_JSON. // // The fields of s may have the struct tags envVar and envDefault. // // - If a field has the envVar struct tag, the value for that field is read // from the environment variable named envVar. // // - If a field has the envDefault struct tag, and the environment variable // is not defined, the field is initialized with the value of envDefault. // If the environment variable is not defined and envDefault is not set, // returns an error. // // If prefix is not empty, both autogenerated and custom environment variable // names are prefixed unless the field has the tag envNoPrefix:"true". prefix is // converted to uppercase, but is otherwise not modified. // // All fields in s must be string, uint, bool, or implement // [encoding.TextUnmarshaler]. Panics otherwise. Returns an error if an // environment variable's value cannot be parsed into the field's type. // // Will only handle fields exported from the struct. func Parse(prefix string, s any) error { prefix = strings.ToUpper(prefix) v := reflect.ValueOf(s).Elem() st := v.Type() if st.Kind() != reflect.Struct { panic(fmt.Sprintf("expected pointer to struct, got type %s", st.Name())) } for _, f := range reflect.VisibleFields(st) { if !f.IsExported() { continue } envVar := f.Tag.Get("envVar") envDefault, hasDefault := f.Tag.Lookup("envDefault") p := prefix + "_" if f.Tag.Get("envNoPrefix") == "true" { p = "" } if envVar == "" { envVar = convertCase(p, f.Name) } else { envVar = p + envVar } vf := v.FieldByName(f.Name) if f.Type.Kind() == reflect.Struct { if f.Anonymous { continue } err := Parse(envVar, vf.Addr().Interface()) if err != nil { return err } continue } val := os.Getenv(envVar) if val == "" { if !hasDefault { return fmt.Errorf("env var %s must be defined", envVar) } val = envDefault } ft := f.Type.Kind() switch ft { case reflect.String: vf.SetString(val) case reflect.Bool: b, err := strconv.ParseBool(val) if err != nil { return fmt.Errorf("env var %s should be a bool: %w", envVar, err) } vf.SetBool(b) case reflect.Uint: i, err := strconv.ParseUint(val, 10, 64) if err != nil { return fmt.Errorf("env var %s should be a uint: %w", envVar, err) } vf.SetUint(i) default: unmarshaler := vf.Addr().MethodByName("UnmarshalText") if !unmarshaler.IsValid() { panic(fmt.Sprintf("unsupported type %s (of field %s) does not implement encoding.TextUnmarshaler", f.Type, f.Name)) } e := unmarshaler.Call([]reflect.Value{reflect.ValueOf([]byte(val))})[0] if !e.IsNil() { return fmt.Errorf("env var %s should be convertible to %s: %w", envVar, f.Type.Name(), e.Interface().(error)) } } } return nil }