diff --git a/envstruct.go b/envstruct.go --- a/envstruct.go +++ b/envstruct.go @@ -14,7 +14,6 @@ if prefix != "" { envVarBuf.WriteString(prefix) - envVarBuf.WriteRune('_') } skip := false @@ -39,23 +38,25 @@ // so that your environment variables do not clash with others. // // Automatically converts struct field names to environment variable case, using -// the following algorithm: the prefix is added, if any, then 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 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. prefix is not prepended to -// envVar. +// 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 @@ -63,6 +64,7 @@ // // 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 { @@ -75,9 +77,15 @@ envVar := f.Tag.Get("envVar") envDefault := f.Tag.Get("envDefault") + p := prefix + "_" + if f.Tag.Get("envNoPrefix") == "true" { + p = "" + } if envVar == "" { - envVar = convertCase(prefix, f.Name) + envVar = convertCase(p, f.Name) + } else { + envVar = p + envVar } vf := v.FieldByName(f.Name) if f.Type.Kind() == reflect.Struct { diff --git a/envstruct_test.go b/envstruct_test.go --- a/envstruct_test.go +++ b/envstruct_test.go @@ -8,7 +8,7 @@ ) func TestConvertCase(t *testing.T) { - const prefix = "MY_APP" + const prefix = "MY_APP_" var tests = []struct { input string expected string @@ -28,7 +28,7 @@ }) t.Run(fmt.Sprintf("%s with prefix", tt.input), func(t *testing.T) { output := convertCase(prefix, tt.input) - expected := prefix + "_" + tt.expected + expected := prefix + tt.expected if output != expected { t.Errorf("got %s, expected %s", output, expected) } @@ -45,7 +45,7 @@ Port uint BufSize uint Log logOpts - Custom string `envVar:"MY_VAR"` + Custom string `envVar:"MY_VAR" envNoPrefix:"true"` IP net.IP } type inputs struct { @@ -66,7 +66,6 @@ { "simple", inputs{"443", "64", "~/.cache/my_app/log", "0", "fun", "127.0.0.1"}, - // testStruct{443, 64, "~/.cache/my_app/log", false, "fun", net.IPv4(127, 0, 0, 1)}, testStruct{443, 64, logOpts{"~/.cache/my_app/log", false}, "fun", net.IPv4(127, 0, 0, 1)}, false, }, @@ -96,21 +95,20 @@ }, } - // TODO: update tests and docs for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var s testStruct - t.Setenv("PORT", tt.values.port) - t.Setenv("BUF_SIZE", tt.values.bufsize) + t.Setenv("PREFIX_PORT", tt.values.port) + t.Setenv("PREFIX_BUF_SIZE", tt.values.bufsize) if tt.values.logfile != "" { - t.Setenv("LOG_FILE", tt.values.logfile) + t.Setenv("PREFIX_LOG_FILE", tt.values.logfile) } - t.Setenv("LOG_USE_JSON", tt.values.useJSON) + t.Setenv("PREFIX_LOG_USE_JSON", tt.values.useJSON) t.Setenv("MY_VAR", tt.values.custom) - t.Setenv("IP", tt.values.ip) + t.Setenv("PREFIX_IP", tt.values.ip) - err := Parse("", &s) + err := Parse("prefix", &s) t.Log(s) if err != nil && !tt.errExpected { t.Errorf("got parse error: %s", err)